回顾和今天的计划
我们没有使用任何游戏引擎和库,完全靠我们自己,使用的是老式的编程方式。
我们已经构建了很多内容,游戏引擎开发也慢慢接近尾声。现在我们已经接近完成了所有为支持游戏开发所需要的工作,接下来将逐步过渡到游戏编程部分。不过我们仍然有几个重要的任务要完成,尤其是与资产系统相关的内容。
昨天我们做了一个很大的进展,完成了资产系统的转换。新的资产系统使用固定数量的内存来运行,这样当需要加载新的资产时,它会释放一些不再使用或近期未使用的资产空间,并加载新的资产。我们已经实现了这一点,但实现方式是通过操作系统的虚拟页表来代替一般用途的内存分配器进行内存管理。对于64位系统来说,这种做法是可行的,但我对32位系统的兼容性并不是百分百有信心。遗憾的是,目前我们没有很好的方式来测试这一点,因为游戏还没有完全开发完成,我们也无法在32位系统上直接运行来检查资产加载和使用模式是否会产生问题。
因此,现在还不清楚在32位系统下,是否会出现内存管理问题。不过,基于目前的实现,我认为64位系统应该是没问题的。
今天的计划:编写我们自己的通用内存分配器
今天的目标是编写一个通用的内存分配器,以便在当前的资产管理系统中使用。虽然目前的系统依赖于操作系统的内存管理机制,但为了更好地理解内存分配的原理,同时保持灵活性,使得可以在自定义内存管理和操作系统的内存管理之间自由切换,决定探索自行实现内存分配的可能性。
当前的代码已经基本实现了资产的加载与释放,并且系统能够在有限的内存占用下正常运行。资产的分页管理机制已经能够确保在内存受限的情况下合理分配和释放资产,并且系统能够稳定运行,没有出现明显的问题。
目前,资产系统的内存分配是通过 acquire_asset_memory
和 release_asset_memory
这两个函数完成的。它们的作用分别是向操作系统申请内存页来存储资产,以及在不再需要某个资产时释放相应的内存页。由于这些函数完全依赖操作系统的内存管理,因此如果要实现自定义内存管理,只需要用自己的实现替换这两个函数,使其能够在内部管理内存,而不是直接向操作系统申请或释放页面。
这样做的好处是可以更高效地管理资产存储,减少与操作系统的交互开销,同时提供更好的内存利用率。未来的任务就是设计一种合适的分配策略,以便在 acquire_asset_memory
和 release_asset_memory
内部实现高效的内存管理。
堆栈内存分配的赞歌
目前的目标是编写一个通用的内存分配器,以取代现有的操作系统内存管理方式。当前资产系统的内存分配是完全通用的,资产的分配和释放可以在任何时间发生,没有严格的顺序约束,这就意味着无法使用栈式结构进行管理。因此,需要一个能够处理随时分配和释放的通用分配器。
在大多数情况下,代码中使用的是栈式内存管理方式,这种方式带来了诸多好处。首先,它消除了生命周期管理的复杂性,因此不需要垃圾回收机制或智能指针来管理内存释放。其次,由于数据的分配和释放都遵循严格的顺序,不需要额外的内存分配策略来决定数据存放的位置,从而避免了额外的性能开销和管理复杂性。
然而,在资产系统中,由于资产的加载和释放并不遵循严格的顺序,因此无法采用栈式管理方式,而是需要一个通用的分配器来处理内存的动态分配与回收。新的分配器需要能够管理不定时释放的内存块,并尽可能减少内存碎片,以提高整体的内存利用率和性能。
下一步的重点将是设计和实现这样的内存分配器,使其能够高效地处理资产的动态管理,同时保持系统的简洁性和高效性。
资产系统需要更复杂的内存分配器
资产系统并不存在生命周期管理问题,因为所有相关的生命周期逻辑都已经被明确定义,并且系统内部已经实现了最近最少使用(LRU)策略来管理资产。因此,资产管理本质上类似于一个虚拟内存管理方案,已经确保了适当的生命周期控制。因此,不需要额外的智能指针或垃圾回收机制,因为生命周期管理问题已经通过特定的算法解决,例如循环链表结构等。这使得额外的自动化管理机制变得冗余且不必要。
然而,尽管生命周期管理问题已经解决,但仍然存在内存分配位置的问题。在资产管理系统中,需要考虑如何合理地在内存中放置数据,以便高效分配和释放。例如,如果资产系统只能使用一大块预分配的内存,那么当请求分配新资产时,该如何管理内存?当释放资产时,又如何处理这部分内存以避免碎片化?
这就需要设计一套合理的内存管理策略,使其能够动态地处理内存分配和回收,同时最大程度地减少碎片,提高整体的内存利用率。这一部分仍然是当前需要解决的问题,即在固定的内存区域内高效地分配和释放资产数据。
资产驱逐和内存碎片化,使用空闲链表
在资产管理过程中,我们面临的一个根本问题是如何在有限的内存空间中合理地分配和回收资产。假设我们依次分配了资产 A、B、C、D 和 E,它们的大小各不相同。当需要分配一个新的资产 H 时,如果内存已满,我们必须通过驱逐(eviction)旧的资产来腾出空间。
通常,我们会优先驱逐最近最少使用(LRU)的资产。例如,如果 B 是最久未使用的资产,并且它的大小等于或大于 H,我们可以直接将 H 放入 B 释放出的空间。然而,问题在于,如果 B 的大小小于 H,那么 H 无法直接替换 B,此时我们需要继续驱逐更多的资产,例如 A、E 或其他合适的资产,直到能为 H 腾出足够的空间。
这就带来了一个复杂的问题,即如何决定应该驱逐哪些资产才能最有效地利用内存。例如,如果 B 是最久未使用的,但它的大小不足以容纳 H,而 E 和 F 加在一起的大小可以满足 H 的需求,那么我们应该选择驱逐 E 和 F,而不是 B。这种决策并不简单,因为我们需要考虑资产在内存中的位置,以及驱逐多个资产后是否真的能有效利用空间。
更复杂的情况是,如果最久未使用的资产依次是 E、B 和 F,我们可能需要跳过 B,仅驱逐 E 和 F,以便为 H 腾出空间。这意味着我们必须在驱逐前进行精确的计算,以确保释放的内存块可以整合为一个足够大的区域,而不是浪费不必要的资源。
这一问题涉及内存管理中的碎片整理和内存块合并等技术,是一个独立且复杂的研究领域。如果要在极限条件下优化内存占用,并最大程度地保留资产,就需要深入研究这一问题,设计高效的算法和数据结构。例如,我们可以尝试将资产文件划分为固定大小的块,以确保加载的资产具有相似的大小,从而简化内存管理。
然而,在当前的开发环境下,我们并不需要深入优化这一问题,因为我们的内存资源相对充足,不需要极端优化资产管理策略。我们主要是意识到这个问题的存在,并理解在需要时可以采取的解决方案,而不必投入大量时间去精细优化这一部分。
我们方法的概述
在实现这样一个内存管理系统时,我们可以从一个最基本的思路开始:首先,我们假设有一块巨大的空闲内存区域可供使用。然而,在实际情况下,尤其是在 32 位 Windows 系统上,我们可能无法获取一整块连续的 1GB 内存。因此,我们可能需要从操作系统获取多个较小的内存块,而不是依赖单一的大块内存。
例如,我们可能会得到多个 64MB 或 256MB 的内存区域,这些区域共同构成我们的资产存储池。为了管理这些内存区域,我们需要维护一个空闲列表(free list),它记录当前可用的内存块。当需要分配新的内存时,我们会从空闲列表中寻找足够大的内存块进行分配,并相应地更新该块的剩余可用空间。例如,如果某个 64MB 的块被分配了 5MB,那么它的可用空间就变为 59MB。
在最简单的情况下,我们可以按照堆栈的方式使用这些内存块:当一个块被填满时,我们就转向下一个空闲块进行分配。这种方式简单且高效,但当我们开始释放内存时,情况就会变得复杂。我们需要维护一个结构,记录当前哪些内存区域是空闲的,哪些是已分配的。当新的内存请求到来时,我们需要检查这些空闲区域,看是否有合适的块可以容纳新的分配请求。如果某个块足够大,就直接在该块上分配,否则我们可能需要合并多个小块,或者寻找新的分配策略。
这种方法的关键点在于如何高效地管理和维护空闲列表,以及如何在碎片化问题发生时做出最佳决策。碎片化可能导致即使有足够的总内存,我们仍然无法找到足够大的连续区域来分配新对象。因此,我们可能需要引入合并(coalescing)和重新整理(defragmentation)机制,以优化内存使用。
总的来说,这种内存管理方法涉及多个重要部分:
- 内存块获取:从操作系统获取多个较小的内存块,而不是依赖单一的大块。
- 空闲列表管理:维护一个数据结构,记录当前哪些内存块可用,以及它们的大小和位置。
- 分配策略:在空闲列表中找到合适的块进行分配,可能需要按照首次适应(first-fit)、最佳适应(best-fit)或最坏适应(worst-fit)等策略进行分配。
- 碎片整理:当空闲内存块过度碎片化时,合并相邻的空闲块,以提高内存利用率。
这个系统的设计是一个权衡问题,需要在性能、内存利用率和实现复杂度之间找到最佳平衡点。
从空闲碎片的角度思考可用内存
当我们开始考虑内存碎片化的问题时,情况会变得更加复杂。最初,我们可以简单地管理几个完整的空闲块,但随着内存分配和释放的进行,内存中的可用空间可能会变得支离破碎。
例如,如果一个内存块中已经分配了一些数据,然后我们释放了其中间的一部分,甚至还释放了顶部的一部分,这样我们实际上就形成了多个分散的空闲片段。这些片段并不是一个连续的整体,而是多个小块。
从管理的角度来看,我们可以把内存视为由一系列不同大小的块组成,每个块要么是空闲的,要么是已分配的。而在实际分配时,我们并不关心那些已经填充的数据,而是只需要关注空闲块的大小和位置,以判断是否能满足新的分配请求。
因此,我们的内存管理系统主要需要关注以下几个关键点:
- 空闲块管理:随着内存的使用,我们需要维护一张记录所有空闲块的信息表,包括它们的位置和大小。
- 分配策略:当有新的内存请求时,我们需要在空闲块列表中寻找合适的块来满足需求。例如,我们可以采用:
- 首次适应(First-Fit):找到第一个足够大的空闲块并分配它。
- 最佳适应(Best-Fit):找到最小但足够大的块,以减少碎片化。
- 最坏适应(Worst-Fit):选择最大的空闲块,以保留较大的连续空间。
- 碎片整理(Defragmentation):当空闲块过度分散时,我们可能需要合并相邻的空闲块,以创建更大的可用空间。
- 释放机制:当内存被释放时,我们需要将其标记为可用,并检查是否可以与相邻的空闲块合并,以减少碎片化。
本质上,我们的内存管理系统的核心目标是最大化内存利用率,同时保持分配和释放操作的高效性。
合并连续的空闲内存块
当我们释放内存块时,内存管理的复杂性进一步增加。假设在某个内存块中,我们已经填充了部分区域,而其他区域仍然是空闲的。例如,我们有两个分开的空闲区域 A 和 B,它们之间被已分配的内存所分隔。在这种情况下,内存分配仍然是可行的:我们可以分别检查 A 和 B,看看是否能容纳新的分配请求。
然而,问题出现在某个中间的已分配块被释放的情况。当这个中间块被释放时,原本分隔的 A 和 B 现在可以合并成一个更大的连续空闲块。这意味着,我们的内存管理系统需要一种机制来检测相邻的空闲块,并在释放操作后合并它们,以形成更大的可用空间,从而减少碎片化问题。
在这种情况下,我们的内存管理系统需要具备以下能力:
跟踪已分配和未分配的空间:
- 在分配时,我们只需要关注空闲块的大小和位置。
- 但在释放时,我们必须知道哪些区域是已分配的,以便判断是否可以合并相邻的空闲块。
合并相邻的空闲块:
- 当一个内存块被释放时,我们需要检查它的前后是否有其他空闲块。
- 如果有,我们应该将它们合并成一个更大的连续块,以提高内存利用率。
- 例如,如果 A 和 B 之间的块 C 被释放,那么 A、B、C 可以合并成一个更大的空闲块,供后续分配使用。
优化内存碎片管理:
- 由于资产系统的特性,我们可能会频繁分配和释放不同大小的块,因此碎片化是一个需要关注的问题。
- 通过维护一个高效的数据结构(如空闲块链表或平衡树),可以快速找到合适的空闲块进行分配,并在释放时高效地合并相邻空闲块。
考虑资产管理的特殊性:
- 由于我们正在实现一个资产管理系统,我们可以根据资产的生命周期和访问模式,设计更适合的分配策略,例如预分配特定大小的块,以减少碎片化的影响。
综上所述,我们的内存管理系统不仅需要高效地分配和释放内存,还需要具备智能的合并机制,以减少碎片化,提高整体的内存利用率。
我们不会进行小规模分配
在我们的内存管理系统中,我们不需要处理小型资产的分配问题。换句话说,我们无需担心有人会请求分配 1 字节或 16 字节的小内存块。所有的分配请求都会涉及较大的块,例如 64KB 或更大的内存区域。因此,我们可以预先设定一个假设,即所有的空闲空间都足够容纳较大规模的数据块,而不会出现碎片化到无法使用的情况。
这一特点带来了几个优化点:
简化内存管理逻辑:
- 由于不需要管理大量小碎片,我们可以使用更简单的数据结构来追踪内存的分配情况,例如使用空闲块列表或区间树来高效管理内存。
- 我们不需要复杂的内存池机制,也不需要像通用内存分配器那样进行大量的小块合并和分裂操作。
减少碎片化影响:
- 由于每次分配的块都是较大的,内存碎片化的影响相对较小。即使出现碎片,空闲区域的大小也通常足够容纳新的分配请求,从而减少无法使用的小碎片的情况。
提高分配效率:
- 在查找合适的空闲块时,我们可以采用简单高效的策略,例如首次适配(First-Fit)或最佳适配(Best-Fit)方法,而不必设计复杂的碎片回收机制。
- 由于没有小型分配请求,我们可以直接操作大块内存,提高 CPU 缓存的利用率,减少分配开销。
优化内存回收策略:
- 由于资产通常具有一定的生命周期,我们可以利用生命周期信息优化内存回收。例如,使用批量回收策略,而不是每次释放小块时都进行碎片合并,从而减少管理开销。
总体而言,这种设计使得内存管理更加高效和可预测。通过限制最小分配单元的大小,我们避免了传统内存分配器中常见的小块碎片化问题,使得整个系统的性能更加稳定和可控。
每个内存块都会保留邻近块的信息
在内存管理的实现上,我们可以考虑一种简单但可能较低效的方法,即在内存块内部存储关于其自身和相邻块的信息,以便进行合并操作。这种方法的核心思想是,每个空闲的内存块都包含一定的元数据,使得在释放时,我们能够快速判断是否可以与相邻的空闲块合并。
内存块的结构
每个内存块不仅存储数据,还可以在其头部或尾部添加额外的元数据,例如:
- 块的大小:记录当前内存块的大小,便于计算相邻块的位置。
- 是否空闲:标记该块是否已被释放。
- 指向相邻块的指针(可选):用于快速找到上一个和下一个块,提高合并效率。
这种方法的主要优点在于:
减少额外的存储需求:
- 我们不需要在额外的全局数据结构(如哈希表或链表)中存储内存块信息,而是直接将管理信息嵌入到每个内存块中,使得整个分配器更加紧凑和高效。
快速合并空闲块:
- 当释放一个内存块时,可以直接检查相邻块的状态。
- 如果相邻块也是空闲的,就可以合并,形成更大的连续空闲块,从而减少碎片化。
适应动态内存需求:
- 由于元数据存储在内存块本身,我们不需要预先设定固定数量的块,而是可以根据需求动态划分和回收内存。
内存回收和合并策略
当一个内存块被释放时,我们需要执行以下步骤:
- 检查上方相邻块是否空闲(向上合并)。
- 检查下方相邻块是否空闲(向下合并)。
- 如果上、下块都为空闲,则合并三个块,形成更大的连续空闲空间。
- 更新合并后的块的元数据,确保后续的分配可以正确识别该块的大小和状态。
这一操作本质上是一个 边界标记合并(Boundary Tag Merging) 机制,它允许我们在释放内存时动态合并相邻空闲块,从而减少碎片化,提高内存利用率。
实际应用中的优化
虽然该方法可以有效管理内存,但仍有一些可以优化的地方:
- 双向链表管理空闲块:可以通过维护一个空闲块的链表,使得分配和回收操作更加高效。
- 分级空闲块列表:根据块大小维护不同的空闲块列表,使得查找合适大小的块更快。
- 延迟合并策略:在某些情况下,立即合并可能不是最佳选择,可以使用延迟合并策略,在需要时才合并块。
总结
这种方法提供了一种 简洁且直接的内存管理方式,通过在内存块内部存储元数据,使得分配、回收和合并操作更加高效,同时减少了外部存储结构的依赖。通过合理的优化,该方法可以在 减少碎片化、提升内存利用率 和 提高操作效率 之间取得良好的平衡。
实现分配操作的一种可能方法
在考虑内存分配时,我们面临的问题并不简单。假设我们已经有了内存块,且这些块是以链表的方式相互连接的,包含了空闲块的信息。当需要分配内存时,我们就会遍历这些空闲块,查找合适的内存块来存放数据。
内存分配的简单思路
在最简单的情况下,我们遍历所有空闲块,找到第一个合适的块来存储所需数据。通常,我们会选择大小最接近需求大小的块,因为这样可以减少内存碎片的产生。通过这种方式,我们在分配内存时实际上是在评估每个空闲块是否足够适合分配请求,并且评估剩余内存是否能合理利用。
了解资产大小分布
由于系统处理的是资产(比如图像或音频文件等),我们可以通过预先扫描所有资产来了解它们的大小分布。例如,假设系统中有大量的4K和8K资产,少量16K或32K资产,这时我们就可以根据这种分布来做出更好的内存分配决策。例如,如果剩余空间是17K,而我们知道大部分资产都大于16K,那么留下一个17K的空闲块可能就没什么意义,因为大多数资产无法适配这个大小。因此,了解资产的实际大小分布,可以帮助我们做出更合理的内存分配判断。
更复杂的内存管理
虽然这个过程看起来简单,但实际上随着系统的复杂性增加,内存管理也变得更为复杂。例如,背景线程的引入使得内存的分配和释放变得更加困难。我们不能保证在分配内存的同时能够立即释放某些内存块,这种情况会让内存管理变得更加复杂。
碎片问题
在这种内存管理策略下,碎片化是不可避免的。当释放内存块时,如果内存块的碎片较小,并且无法填充其他资产大小时,就会导致浪费。因此,如何有效地管理这些碎片,避免内存浪费,成为了一个需要解决的问题。为了减少碎片,可能需要在合适的时机对内存块进行合并操作,合并相邻的空闲块来形成更大的空闲空间,这样可以提高内存的利用率。
内存管理的复杂性
相比其他系统部分,比如渲染系统,内存管理显得更加复杂。渲染系统虽然数学上非常复杂,但它是一个相对独立的过程,而内存管理系统则涉及到多个互相交织的部分,需要处理的情况更加复杂,特别是在资产数据量大的情况下。背景线程的使用进一步增加了复杂性,使得内存分配和释放之间的时序问题变得难以控制。
总结
内存分配和管理看似简单,但实际上涉及到很多复杂的决策,尤其是当资产大小不均匀且系统具有背景线程时。通过预先了解资产大小分布,可以做出更加高效的内存分配决策,而在实际操作中,内存的碎片化和合并策略则是解决这一问题的关键。在处理这些内存管理的挑战时,可能需要不断优化和调整策略,以便提高内存的利用效率并避免不必要的浪费。
我们可以在 AcquireAssetMemory 内部调用 EvictAssetsAsNecessary!
在思考如何管理资产的内存时,存在一些误解和假设需要澄清。之前认为,在某些情况下,可能无法在资产加载的过程中进行内存释放,但经过进一步的思考和分析,发现其实在加载资产时,资产的状态会设置为“排队”(queued),并且不会释放处于“排队”状态的资产。因此,在资产处于“排队”状态时,实际上是可以在内存中释放其他资产的。这样一来,内存管理的灵活性可能比原先想象的要高得多。
释放资产的时机
实际上,释放资产并不需要等待其他操作的完成。因为资产只会在主线程中使用,且只有在实际使用时才会锁定,所以在加载资产时,可以随时释放不再需要的资产。特别是在处理内存时,我们可以在加载新资产之前,通过适当的时机进行资产的驱逐(eviction),而不需要提前调用专门的释放函数。这样,内存管理的过程变得更加简化。
内存获取与驱逐操作
为了确保内存的有效使用,可以在获取资产内存的过程中,按需驱逐那些不再使用的资产。这个操作可以在“获取资产内存”时执行,这样一来,我们可以确保在每次进行内存分配时,内存不会因为无用的资产而被浪费。通过这种方式,内存的分配和释放可以动态进行,避免了过多的预处理工作。
简化代码结构
根据这种思路,可以进一步简化代码中的某些部分。比如,原本需要在特定位置调用的“如果必要则驱逐资产”函数,现在可以直接去掉,因为这一过程已经可以通过资产内存获取的操作来处理。这就使得代码更加简洁,并且可以减少不必要的操作。
测试和多线程问题
虽然这种方法看起来可行,但测试过程中的复杂性依然存在,特别是涉及多线程操作时,单纯运行测试并不能百分百保证没有潜在的竞争条件(race condition)。即使测试没有立即失败,也不能确认其完全正确。因此,在多线程环境下,虽然这种简化的方式理论上是可行的,但仍然需要谨慎,并确保所有的竞争条件被妥善处理。
总结
通过对内存管理的深入思考,发现可以更灵活地管理内存,特别是在资产加载的过程中,内存的释放和驱逐可以与资产的加载过程同步进行,这样不仅简化了代码结构,也提高了内存管理的效率。然而,尽管这种方法理论上是可行的,在实际运行中仍需谨慎处理,特别是在涉及多线程的情况下,需要确保没有竞争条件和其他潜在问题。
修改 AcquireAssetMemory 以适应我们自己的内存管理
在重新设计内存分配机制时,目标是将资产内存的分配改为从自定义的内存区域(而不是操作系统的内存管理)进行分配。之前使用的是“arena”(内存池)模式,但现在打算放弃这种方式,因为它不适用于当前需求,特别是由于之前使用的栈式分配方式不再适用。因此,打算直接分配一块内存区域,并用结构体表示这一内存块的元数据。
设计资产内存块
首先,设计了一个新的结构体asset_memory_block
,该结构体包含内存块的总大小和已使用的大小。这个结构体的作用是管理每个资产内存块的元数据,尤其是用于追踪已分配内存的大小和剩余内存的大小。为了简化管理过程,内存分配块的大小与用于存储头部信息的空间进行区分。
分配内存的实现
然后,在分配资产内存时,计划采用以下步骤:
- 计算内存块的总大小,并为这个内存块分配空间。
- 创建并初始化一个
asset_memory_block
,记录内存块的总大小和已使用的大小。 - 从内存池中获取一块内存,并将其地址与这个内存块进行关联。
初始化内存块
初始化时,将内存块的总大小设置为申请的内存大小减去内存块本身的大小。已使用的大小从一开始就是零,因为没有分配实际的资产内存。这样,内存管理机制能够开始跟踪内存块的使用情况。
分配内存时的检查
在进行内存分配时,分配的大小会更新Block->UsedSize
,即已经使用的内存量。如果已使用的内存超出了总内存大小,就会触发一个断言,确保不会出现内存溢出。这个断言检查的目的是避免在内存块已满的情况下继续分配,从而避免未定义行为的发生。
断言机制
通过断言确保内存分配的有效性,系统会检查已使用的内存是否超出分配的内存总量。如果超出,程序会终止并报告错误,表明内存不足。
修正问题
在测试时发现,Block->UsedSize
的计算有错误,导致实际使用的内存超出了分配的内存。这个问题通过调整计算公式解决,即正确地将已使用的内存大小与请求的内存大小相加,确保总使用内存不超过分配内存。
总结
通过这种方法,可以确保资产内存的管理更为精确,同时避免了使用外部平台内存分配的复杂性。整个内存管理过程通过自定义的内存块结构来控制,不仅简化了内存分配过程,还增强了对内存使用的精确跟踪,避免了内存溢出的风险。这种方法在内存分配过程中能够灵活应对不同的资产大小和内存需求。
驱逐资产以为新资产腾出空间
在内存分配过程中,目的是在内存即将满时触发回收机制,而不是在分配内存时立即断言失败。当前的做法是,在尝试分配内存时,如果发现当前内存块的已使用空间加上请求的内存大小会导致溢出,则应该在内存溢出的情况下开始执行资产回收操作。具体来说,当无法在当前内存块中找到足够的空间来分配新的资产时,才开始清理不再使用的资产,释放内存,以便为新的资产分配腾出空间。
详细过程:
内存溢出检测:
在分配内存之前,首先检查当前内存块的已用大小加上请求分配的大小是否会超出该内存块的总大小。如果会超出,则说明没有足够的内存可以分配。触发回收:
如果检测到内存溢出,即没有足够空间分配新的资产时,系统将开始执行资产回收操作。回收机制会尝试释放一些已经不再使用的资产,从而释放出足够的内存来容纳新的资产。实现方式:
这时的回收操作不再是一个断言失败的情况,而是在实际检测到溢出后才会触发,确保内存管理更加灵活,不会在分配阶段就因为内存不足而强制终止程序。
总结:
此方法通过延迟到内存溢出时才开始回收资产,从而提高了内存管理的效率和灵活性。在内存即将达到极限时触发回收,可以确保程序不会在正常的内存分配过程中就因溢出问题停止,从而避免了不必要的断言错误。
跟踪我们资产的位置
在内存管理中,目前没有办法释放内存,因为我们不知道每个块的位置,因此我们需要一种方法来跟踪和管理这些内存块。为了实现这一目标,我们开始考虑引入一个“资产内存块标记”(Asset Memory Block Marker)。这个标记类似于一种标识符,用于帮助我们追踪每个内存块的使用情况,并能够在必要时执行内存释放操作。
具体实现步骤:
引入标记:
我们会在每次分配内存时插入一个资产内存块标记。这个标记实际上是一个简化的内存管理结构,能够记录每个内存块的大小、使用状态,以及它在内存中的位置。我们不再使用以前的堆栈结构,而是采用这种更加灵活的链表式结构。资产内存块的结构设计:
每个资产内存块(Asset Memory Block)将包含以下内容:- 大小:记录该内存块的总大小。
- 已使用大小:记录该内存块已被使用的内存量。
- 前后指针:这些指针帮助我们在内存中跟踪每个内存块的位置,形成一个链表结构,便于内存块的遍历和管理。
- 标志位:标记该内存块是否已经被使用。通过这个标志位,系统可以判断该内存块是否处于空闲状态。
内存块创建过程:
在创建每个资产内存块时,首先会将当前内存块的前后指针初始化为空,并且会设置当前块的大小。同时,我们需要管理每个块的使用状态,标记它是否已被占用。在首次创建时,使用状态为“未使用”。内存分配:
当内存被分配时,新的资产内存块标记会插入到合适的位置,以确保内存块的管理逻辑正确。通过链表指针,我们能够追踪每个内存块,并在必要时进行回收操作。内存回收:
使用这种标记的方式,在进行内存回收时,可以通过遍历这些标记来判断哪些内存块可以释放。当某个内存块标记为“未使用”时,就可以将其释放,腾出空间供新的资产分配。
总结:
通过引入资产内存块标记,我们能够在内存管理中更好地跟踪和管理每个内存块。这种方法虽然简单,但为今后的内存回收提供了基础,避免了内存溢出问题。通过链表结构,系统可以高效地追踪内存块的位置,并执行灵活的内存分配与回收操作。
双向链表内存块
在内存管理的过程中,为了更灵活地管理内存块,首先,我们采用了一种循环链表的结构。通过这种方式,每个内存块不仅有指向前一个和下一个内存块的指针,而且形成了一个环形结构,便于内存块的管理和访问。这种结构在某些情况下可以提高内存块的访问效率,同时使得内存的插入和删除操作更加简便。
内存块插入:
创建内存块标记:
首先,我们定义了一个内存块标记,它包含了内存块的大小、状态标志位、前后指针。标记的前后指针将会指向自己,以形成一个环形链表。在这个内存标记的创建过程中,我们将标志位初始化为0,大小为0,前后指针指向自己。插入内存块:
当我们要插入一个新的内存块时,我们调用InsertBlock
函数。该函数的作用是将新内存块插入到链表中。在插入过程中,需要确保当前块的大小足够大,以容纳控制块,否则无法插入。具体来说,内存块的大小会减去控制块的大小,剩余的部分用于实际的内存分配。在插入操作中,还需要更新链表的指针,确保新插入的块能够正确地链接到前后内存块中。此时,需要将新块的前指针指向原来的前块,后指针指向原来的后块。同时,将原前块的后指针指向新块,原后块的前指针也指向新块,从而完成环形链表的插入。
内存分配:
查找合适的内存块:
当我们需要分配内存时,首先会通过FindBlockForSize
函数查找是否有足够大的内存块来容纳我们请求的内存。如果找到一个合适的内存块,系统会进行分配,更新使用的内存大小,并将内存块的剩余部分标记为未使用。分配后,继续返回该内存块。处理内存不足的情况:
如果在查找过程中未能找到合适的内存块,就需要开始进行内存回收(eviction)。回收过程涉及从内存链表中找到一个可以回收的内存块,并将其释放。释放时,我们会遍历内存链表,直到找到一个适合回收的内存块。一旦找到合适的内存块,就进行释放,并返回该块给系统,之后继续查找内存块以满足当前的内存请求。内存块的切割:
在分配内存时,如果找到的内存块足够大,我们需要将该内存块切割成两部分:一部分用于当前的分配请求,另一部分保留以备后用。切割过程需要确保剩余部分仍然满足内存块的控制结构,且切割后部分能继续作为新的可用内存块。回收内存:
内存释放时,我们会先尝试在当前内存块中查找适合的空间,如果没有找到,则启动内存回收机制。在回收过程中,我们从链表中找到一个合适的内存块,并进行释放,直到找到合适的空间为止。释放内存后,系统会重新检查是否有空间来分配新的请求。
优化和路径:
优化回收机制:
我们还优化了内存回收的方式,使得在回收时,能够更加高效地找到需要释放的资产内存块。这通过遍历内存链表,按顺序查找可以释放的块,并确保在释放后继续检查链表以保证最大化的内存回收效率。避免内存碎片:
在内存分配和回收过程中,我们还特别注意了内存碎片问题。通过合理的内存块切割和回收策略,尽量减少内存碎片的产生,确保在内存被频繁分配和释放时,内存资源能被最大化利用。
总结:
这种内存管理方式通过循环链表和内存块标记来跟踪内存使用情况,使得内存的分配和释放更加灵活。当内存不足时,系统会通过回收机制回收内存,确保分配请求能够得到满足。通过这种方法,可以有效地管理内存,避免内存浪费,并提高内存使用效率。
FindBlockForSize
在内存管理的过程中,我们实现了一个 FindBlockForSize
函数,用于查找合适的内存块来满足内存分配请求。具体的流程如下:
查找合适内存块的步骤:
遍历内存块链表:
我们从内存的 sentinel(哨兵)开始,遍历所有的内存块。内存链表的每个内存块都有一个标志位Flags
来表示该块是否已经被使用。查找未使用的内存块:
在遍历过程中,如果当前的内存块标志位Flags
表示该块没有被使用(即未分配),并且该块的大小大于或等于我们请求的大小,就认为该内存块可以使用。返回找到的内存块:
一旦找到一个符合条件的内存块,直接返回该块即可。此时,我们不考虑优化,例如寻找最佳匹配块,仅仅是简单地找到第一个合适的内存块并返回。
优化和简化:
不考虑最佳匹配:
在当前的实现中,我们没有进行任何优化来找到最合适的内存块(如最小适配策略),而是简单地返回第一个找到的合适块。这种做法虽然效率较低,但实现起来较为简单且能够快速达到功能需求。跳出循环:
一旦找到合适的内存块,就会立即跳出循环,并返回该内存块。此时不再继续遍历后续的内存块,从而简化了代码流程。
总结:
这个 FindBlockForSize
函数的目的是快速找到一个足够大的、未被使用的内存块,并将其返回。我们没有在当前实现中对内存块的选择进行最优匹配的处理,而是简单地返回第一个找到的符合条件的内存块。尽管这种做法没有考虑性能优化,但它能够确保功能的实现,并为后续的优化留出空间。
根据剩余容量条件性地拆分存储资产的内存块
在内存分配过程中,当我们找到一个合适的内存块时,接下来需要决定是直接使用这个内存块,还是将其拆分成两个部分。具体操作如下:
内存块处理过程:
验证内存块大小:
我们首先通过FindBlockForSize
函数查找合适的内存块后,进行一个断言检查,确保所需的内存大小能够放入该内存块中。这样可以保证分配的内存足够大。获取内存:
如果找到合适的内存块,内存的实际使用部分就是从内存块的起始位置往后size
字节。接下来,我们判断这个内存块是否需要被拆分。判断是否拆分:
如果分配内存后,内存块剩余的空间足够大(大于某个阈值,比如 4096 字节,类似于一个内存页的大小),那么我们就会拆分这个内存块。阈值的大小可以根据实际需求调整,比如可以设定为 4096 字节或更大的值。拆分内存块:
- 如果内存块剩余空间足够大,就将剩余的部分作为一个新的内存块处理。
- 新的内存块会被插入到当前内存块后面,作为一个独立的块,剩余的空间将继续维护为可用内存。
- 在进行拆分时,我们更新内存块的大小,将其减少已分配的内存大小,并设置新的内存块头信息,将其插入到链表中。
更新内存块状态:
在分配内存之后,我们会标记当前内存块为已使用,更新Flags
字段为“已使用”,以确保后续不会将此内存块重复分配。返回内存块:
最终,返回分配给请求的内存区域,如果进行了拆分,那么返回的是分配部分的内存地址。
总结:
当找到合适的内存块时,我们首先确认内存块足够大,如果剩余空间足够用于另一个内存分配(如超过阈值),则拆分当前内存块,并将新的内存块插入到链表中。通过这种方式,我们可以高效地管理内存,并避免浪费内存空间。如果剩余空间较小,则直接使用当前内存块,不再拆分。
在 ReleaseAssetMemory 内恢复内存块
在内存释放的过程中,需要进行一些额外的操作来确保内存块能够正确地回收和合并。具体步骤如下:
释放内存过程:
回收内存块:
- 在
ReleaseAssetMemory
函数中,需要获取到要释放的内存块。这个内存块的位置是通过当前内存地址减去 1 来找到的。这样做可以确保能够正确地找到对应的内存块头信息。
- 在
标记内存块为未使用:
- 一旦找到了对应的内存块,我们需要将该块的
Flags
字段标记为“未使用”(即将Flags
设置为 0),这样就表明该内存块已经被释放,并且可以再次用于其他内存分配。
- 一旦找到了对应的内存块,我们需要将该块的
合并相邻内存块:
- 释放内存后,我们需要检查当前内存块的前后相邻块,看看是否可以进行合并。如果前后相邻的内存块也是空闲的(即未使用),我们就可以将它们合并成一个更大的空闲内存块。这样做可以减少内存碎片,提升内存的使用效率。
- 这一过程涉及到修改相邻内存块的链接,将相邻的空闲内存块连接成一个更大的块,便于后续的内存分配。
总结:
内存释放过程的关键是:
- 回收内存块并将其标记为未使用。
- 合并相邻的空闲内存块,避免内存碎片的产生,提升内存的利用率。
虽然这些操作听起来简单,但实现时可能会遇到一些边界情况和复杂的合并逻辑。因此,在实际开发过程中,可能需要进一步调试和优化。
后面界面会一直闪
鉴于这是一个优化问题,似乎我们需要有一个系统来分析资产使用情况和驱逐情况(并定期检查其输出,随着游戏和资产的增长)。这是否应该尽早安排在议程上?
在面对优化问题时,需要建立一个资产使用和逐出(eviction)系统,并定期检查其输出,尤其是随着游戏和资产规模的增长,持续对性能进行监控。以下是一些关键要点:
资产监控系统的必要性:
资产逐出和分配的统计:
- 我们需要能够追踪每一帧的资产分配和逐出情况。这将帮助我们了解每一帧需要多少资产,以及这些资产是否能够高效地复用。
内存块的使用情况:
- 监控内存块的数量、状态以及内存的碎片化程度。这有助于发现内存管理的瓶颈,及时调整优化策略,避免不必要的内存浪费。
线程状态监控:
- 游戏引擎中的多个线程会执行不同的任务,了解线程的工作情况对于优化性能非常重要。通过查看每个线程的负载,能够判断出哪些部分可能成为性能瓶颈,进一步优化资源分配。
为什么需要立即关注这些问题:
引擎和游戏已经具备了足够的复杂性:
目前游戏引擎已经有了基础的功能,如渲染器、资产系统和声音播放等。这些功能已经足够支撑一个完整的游戏制作,因此是时候开始进行性能分析和优化了。调试工具的必要性:
在这个阶段,增加调试代码是至关重要的。通过这些工具,可以可视化系统的表现,查看各个环节的工作情况,从而及时发现问题并加以优化。这样做有助于我们在后期开发中更好地理解系统瓶颈,进行必要的调整。高质量的需求:
虽然某些如光照等视觉质量的提升并非必需品,但对于一个高质量的游戏来说,确保系统的性能和内存管理是不可或缺的。没有这些监控工具,游戏的性能可能会受到影响,进而影响整体质量。
总结:
通过引入性能监控和资产管理系统,可以帮助开发团队实时跟踪内存和资源的使用情况,发现潜在的性能瓶颈。随着游戏开发的推进,及时的优化和调整将是确保游戏质量的重要部分。这些工具不仅有助于开发人员发现问题,还能帮助游戏在最终发布前确保高质量和高性能。
我不太清楚如果我们没有找到内存块时会发生什么,能否再讲解一下?
在没有找到合适的内存块时,系统会进入一个“else”分支来处理这种情况。这个过程大致如下:
处理步骤:
内存块查找失败:
- 如果我们无法在现有的内存块链表中找到一个可以容纳当前资产的内存块(即内存已满或没有合适的空闲块),那么就意味着我们的内存资源已经被大部分填满,或者存在内存碎片化现象。
内存碎片化的处理:
- 在这种情况下,系统会开始遍历所有已加载的资产,并根据“最近使用”的顺序进行处理。换句话说,系统会优先处理最久未使用的资产,因为假设这些资产在下一帧中不太可能再次被访问。这个顺序的设计是为了优化内存使用,确保更少被使用的资源可以优先被逐出。
逐出资产:
- 在检查到一个资产时,如果该资产的加载标志(load flag)有效,表示该资产已经完全加载并且不是正在被后台任务处理的中间状态,此时可以将其逐出。
- 逐出后,该资产占用的内存会被释放,从而为新的资产腾出空间。
重新查找内存块:
- 当一个资产被逐出后,内存中腾出了空间,这时系统会重新进行内存块查找,尝试再次寻找合适的空闲内存块。此时,我们重新开始遍历内存块链表,并查看是否有新的可用内存块来存储当前的资产。
避免重复查找:
- 系统在释放内存后,理论上只需要重新检查刚才被释放的内存块即可。因为在释放内存之前,系统已经确定没有其他内存块可以容纳新的资产。所以,一旦有内存释放,我们只需要关注释放后得到的新的空闲内存块,而不需要重新遍历所有内存块。这就是为何在这个地方特别标注“TODO”,表示需要进一步完善这个逻辑。
总结:
整体而言,当内存满了,系统通过逐出最近未使用的资产来释放内存空间。这一过程确保了在内存资源紧张时,系统会优先清理那些可能不会立即再次使用的资产,从而为新的资产腾出空间。这种机制不仅能减少内存浪费,还能有效避免内存溢出。
为什么我们需要检查剩余大小是否大于阈值?为什么不直接使用剩余大小,只要它满足请求的大小就行?
在讨论内存块的剩余大小时,提出了两个不同的概念:内存剩余大小和阈值。这两个值分别代表不同的逻辑用途,理解它们的区别有助于正确处理内存分配和释放。
内存块大小与请求大小的关系:
- 当我们通过
FindBlockForSize
函数查找合适的内存块时,系统假设返回的内存块能够满足当前资产的大小要求。也就是说,一旦找到合适的内存块,资产就直接被放置在该块中,标记该内存块为已使用。
剩余空间与阈值的区别:
- 剩余空间:这是内存块中未被使用的部分,它代表当前内存块在存储资产后所剩余的空闲区域。系统会评估剩余空间是否足够创建一个新的可用内存块。
- 阈值:这是一个预设的最小大小,用来判断是否值得将剩余空间用于后续的内存块创建。如果剩余空间不足以存放任何资产,那么系统就不会在该位置创建新的内存块。例如,如果系统知道最小资产的大小是16K,那么小于16K的剩余空间就不需要创建新的内存块,因为这样的空闲空间无法容纳任何资产。
为什么需要阈值:
- 阈值的目的是避免浪费内存和不必要的内存块管理。如果剩余空间过小,无法容纳任何后续的资产,那么就没有必要创建一个新的内存块,因为该内存块将永远无法被有效使用。
剩余空间的进一步利用:
- 但是,剩余空间的管理也并非绝对。假设后续的内存块被释放时,它可能会与当前内存块的剩余空间合并,从而能够存放新的资产。为了更好地管理这种情况,系统可能需要记录每个内存块中未使用的部分,以便在相邻的内存块都为空时进行合并,从而更有效地利用内存空间。
总结:
- 剩余空间和阈值的区别在于,剩余空间表示内存块中的空闲部分,而阈值则用于判断是否值得为这些空闲部分创建新的内存块。通过设置适当的阈值,可以避免无意义的内存块创建,确保内存管理的高效性。同时,系统还可以通过记录未使用的内存部分,并在相邻块空闲时合并它们,从而更好地管理内存碎片问题。
为什么这个内存管理系统仅限于游戏资产,而不是其他方面?
当前的内存管理系统仅限于游戏资产的管理,并不涉及其他类型的内存分配。之所以这样设计,主要是因为目前没有其他部分的代码需要类似的内存管理功能。未来,如果发现有其他代码部分需要进行通用的内存分配(例如为了某些新的功能或需求),那么可以将这一内存管理系统抽取出来并在那时进行适配和扩展。但是,目前并不需要提前做这件事,因此不急于进行这种扩展。
GUI 将处理什么?
GUI(图形用户界面)将负责处理与用户交互的部分。例如,如果有一个需要显示内存使用情况、资产分配或内存管理状态的面板,GUI就会负责渲染这些信息,并提供交互界面让用户查看或调整相关参数。它将处理用户输入,展示内存分配状态、资源加载状态等,并可能提供实时反馈,比如图形化展示内存使用、资产加载进度等,帮助用户了解当前系统状态。
具体到内存管理系统,GUI会显示分配的内存块、已加载和未加载的资产、碎片化情况等,可能还会提供某些控制选项供开发人员或用户调整系统设置,以优化内存使用或排查性能问题。这类功能对于调试和优化非常重要,尤其是当系统中资产增多时,GUI能够直观地展示资源的利用情况。
总的来说,GUI的主要作用是向用户或开发者展示相关的内存和资源状态,提供交互接口,并实时反映系统的内存管理和资产加载状况。
如果这是一个商业项目,是否会使用 malloc 而不是自己写一个来管理资产?
如果这是一个商业产品,是否使用 malloc
还是自己写内存管理系统会依赖于具体的需求。如果确实需要做一个较为复杂的资源管理系统,并且想要更好的控制内存的分配和碎片化问题,可能会考虑写一个自己的分配器,特别是用于游戏资产的分配。然而,通常情况下,开发者倾向于使用固定大小的内存块来进行内存管理,而不依赖于通用的内存分配方法。
如果真的决定需要一个通用的内存分配器,并且这不只是一个实验性的需求,开发者可能不会直接使用 malloc
。虽然 malloc
可以完成分配任务,但由于它是依赖于不同平台上 C 运行时库的实现,可能会导致各个平台的表现不同。因此,可能会寻找开源或者公共领域的内存分配器,来保证其在所有平台上的一致性。
此外,虽然开发者在一些项目中会选择从零开始编写一切代码,尤其是像游戏引擎这样的定制项目,但这并不意味着不能使用信任的第三方库。例如,如果有一个开源的内存分配器,开发者完全可以选择使用它,而不是自己重新实现一个。像 Shawn Barrett 这样的库就被认为是可靠的,因此使用它们不会有什么问题。
总的来说,是否使用 malloc
或者自己写一个内存分配器,取决于对系统控制的需求和对不同平台兼容性的考虑。如果确实需要一个通用的内存分配器,可能会选择一个开源的库,而不是直接依赖 C 运行时库的 malloc
,因为后者可能存在平台差异和不一致性的问题。
是否有可能在游戏或游戏引擎中使用马尔可夫链?
马尔科夫链(Markov chains)可能不适用于当前的情况。马尔科夫链是一种数学模型,用于描述系统从一个状态转移到另一个状态的过程,这个过程依赖于当前的状态,而与历史状态无关。通常,马尔科夫链应用于需要描述随机过程的领域,例如文本生成、天气预测、金融模型等。
然而,在这里所讨论的场景中,可能并不适合使用马尔科夫链。这是因为,内存管理系统和资源分配问题通常不符合马尔科夫链的特性,特别是在游戏资产和内存块管理的背景下,系统的状态转移和内存块的使用是由多个因素决定的,并且可能与历史状态和系统的全局状态有关。
此外,马尔科夫链的实现和分析通常需要有明确的状态空间和转移矩阵,这对于内存管理和游戏资源管理来说,可能显得过于复杂且不适合。内存管理更多依赖于确定性分配策略、内存碎片合并等操作,而这些操作与随机过程并不直接相关,因此使用马尔科夫链来建模和优化内存分配可能并不是最有效的方式。
总体来说,虽然马尔科夫链在某些场合非常有用,但在内存管理系统中,尤其是处理游戏资源和资产时,可能更适合使用其他类型的内存分配算法和策略,而不是引入马尔科夫链的随机过程模型。
如果 Vulkan 按时发布,它看起来会是硬件渲染的一个不错选择。我刚刚读到,你可以控制所有内存,以避免任何未知的分配:“Vulkan 中的显式内存管理允许应用程序使用自定义分配策略。例如,可以在渲染期间避免任何分配,全部预先分配内存。”
Vulkan 在及时发布后可能会成为硬件开发中的一个不错的选择,因为它允许对所有内存进行控制,从而避免任何未知的分配。Vulkan 中的显式内存管理允许应用程序使用自定义分配策略,例如在渲染过程中避免任何内存分配,提前分配所有内存。
不过,有两点需要注意:
首先,关于 Vulkan 的一些问题,由于保密协议(NDA)的原因,无法对 Vulkan 相关内容进行评论。
其次,这个说法可能会有些误导。尽管从某种角度来说,这个说法可能在某些条件下是成立的,但由于保密协议的限制,无法进一步详细说明这个问题。至于谁允许发表类似的说法,自己也不清楚。
你有没有考虑过使用“预制”资产,供人们创建更复杂的资产,比如有功能的门的整座房子?
关于使用预制资产来创建更复杂的资产,比如带有功能门的完整房屋,不太清楚这个意思。
TODO 列表中提到的 GUI
在待办事项中提到的GUI,是指为了在调试过程中查看信息时使用的界面。由于调试时可能会有大量的信息需要展示,这些信息可能无法全部同时显示在屏幕上,或者如果全部显示,会影响游戏的可视化效果。因此,需要一个小型的GUI,帮助选择当前在调试视图中查看的内容。
你一直说“将内存块分成两半”,这是什么意思?‘大小 = 大小/2’吗?
"拆分内存块"的意思是,当找到一个内存块后,需要检查这个块是否足够大。如果这个块的大小大于资产所需的内存大小,那么就会把这个块拆成两部分。具体来说,拆分的过程是这样的:
我们首先找到一个内存块,通常这个块会有一个头部,包括前一个指针、后一个指针、标记和大小等信息。接下来,我们分配资产到这个块中。
资产占用了这个内存块的一部分,剩余的部分仍然是空闲的。因此,为了让剩余的空间可以再次使用,我们需要“拆分”这个块。
拆分的操作是:在资产分配的地方插入一个新的头部,标记剩余的空间是空闲的。新头部的前一个指针指向原来块的头部,原来块的后一个指针指向新创建的块,而新创建的块的后一个指针指向原来块后面剩余的部分。
这样,原本的内存块被拆成了两部分,已经分配给资产的部分被标记为已使用,而剩余的部分则被标记为空闲,重新加入内存块链中,供后续的内存分配使用。
这个过程就是拆分内存块的意思。
这是我从哪里读到的:http://blog.imgtec.com/powervr/trying-out-the-new-vulkan-graphics-api-on-powervr-gpus
https://blog.imaginationtech.com/trying-out-the-new-vulkan-graphics-api-on-powervr-gpus/
从这段内容来看,Vulkan 是由 Khronos Group 开发的下一代高性能图形和计算 API。文章提到,Vulkan 的内存分配与传统的 OpenGL 等 API 的内存管理方式不同。OpenGL 中,当调用 glTexStorage2D
时,驱动程序会为二维或一维纹理分配内存,这个内存分配过程是由驱动程序处理的,开发者无法直接控制。而 Vulkan 则提供了更多的内存控制权,内存分配由应用程序本身来完成,意味着应用程序可以更清楚地知道它使用了哪种类型的内存以及使用了多少内存。这种内存管理方式对于那些对内存有严格要求的应用程序(如内存受限的应用)非常有用,可以更好地优化内存的使用,避免内存分配上的瓶颈。
然而,文章提到的博客内容似乎并不准确或可靠,因此建议完全忽略该博客的信息。
调试 GUI 会有类似 Quake/Unreal 的酷控制台吗?
讨论了调试界面的设计,创建类似于《Quake》或《Unreal》的酷炫控制台并不是一个明智的时间利用,尤其是考虑到当前的工作重点。接下来,对自己实现的内存管理系统感到兴奋,尽管它可能还不完善,但他更倾向于使用自己完全理解的代码,而不是可能更好的,但自己不熟悉的代码。虽然计算机速度很快,内存分配并不会频繁发生,还是觉得能够自己掌握并管理内存是一个值得做的事情。