11

背景:我正在开发一种多平台框架,将用作游戏实用程序/工具创建的基础。基本思想是有一个工人池,每个工人都在自己的线程中执行。(此外,worker 也可以在运行时生成。)每个线程都有自己的内存管理器。

我一直想创建自己的内存管理系统,我认为这个项目将是完美的,最终试一试。由于该框架的使用类型,我发现这样的系统适合通常需要实时分配内存(游戏和纹理编辑工具)。

问题:

  • 没有普遍适用的解决方案(?) - 该框架将用于游戏/可视化(不是 AAA,而是独立/游戏)和工具/应用程序创建。我的理解是,对于游戏开发,通常(至少对于控制台游戏)在初始化时只分配一次大块内存,然后在内存管理器内部使用该内存。但这种技术是否适用于更一般的应用?

    在游戏中,您理论上可以知道您的场景和资源需要多少内存,但例如,照片编辑应用程序将加载各种不同大小的资源......因此在后一种情况下,更动态的内存“块大小”将是需要吗?这让我想到了下一个问题:

  • 移动已分配的数据并保持有效指针- 通常在堆上分配时,您将获得一个指向内存块的简单指针。据我了解,在自定义内存管理器中,类似的方法是返回一个指向预分配块中空闲位置的指针。但是如果预分配的块太小并且需要调整大小甚至碎片整理会发生什么?数据需要在内存中移动,旧指针将无效。有没有办法以某种方式透明地包装这些指针,但仍然像通常的 C++ 指针一样在内存管理“外部”使用它们?

  • 第三方库- 如果无法透明地使用自定义内存管理系统来分配应用程序中的所有内存,那么我链接的每个第三方库仍将在内部使用“旧”操作系统内存分配。我了解到库公开函数以设置库将使用的自定义分配函数是很常见的,但不能保证我将使用的每个库都具有这种能力。

问题:实现可以使用动态大小的内存块池的内存管理器是否可能且可行?如果是这样,碎片整理和内存调整大小如何工作,而不破坏当前正在使用的指针?最后,如何最好地实施这样的系统以与第三方库一起使用?

我也感谢任何相关的阅读材料、论文、文章等等!:-)

4

4 回答 4

10

作为之前为过去几代游戏机编写过许多 AAA 游戏的内存管理器和堆实现的人,让我告诉你,它不再值得了。

您的信息很旧 - 早在 gamecube 时代 [大约 2003 年] 我们曾经按照您所说的去做 - 分配一个大块并使用为每个游戏调整的自定义算法手动分割该块。

一旦虚拟内存出现(xbox 时代),游戏变得更加复杂 [因此分配更多并成为多线程] 地址碎片使这种情况变得站不住脚。因此,我们切换到自定义分配器来仅处理某些类型的请求 - 例如物理内存、无锁小块低碎片堆或最近使用块的线程本地缓存。

随着内置内存管理器变得更好,它变得比那些更好 - 当然在一般情况下和特定用例的接近的事情。Doug Lea Allocator [或任何主流的 c++ linux 编译器现在都带有] 和最新的 Windows 低碎片堆真的非常好,你最好把时间花在其他地方。

我有工作中的电子表格来测量整个分配器负载的各种指标——所有的大牌和我多年来收集的一些。基本上,虽然专家分配器可以在几个指标上获胜(每个分配的最低开销、空间接近度、最低碎片等),但对于整体指标来说,主流分配器是最好的。

作为您图书馆的用户,我个人的首选选择是您只需在需要时分配内存。使用operator new/thenew操作员和我可以使用标准 C++ 机制来替换它们并使用我的自定义堆(如果我确实有的话),或者我可以使用特定于平台的方式来替换您的分配(例如 Xbox 上的 XMemAlloc)。我不需要标记[捕获调用堆栈要优越得多,如果我愿意,我可以做到]。在该列表的下方,您为我提供了一个接口,当您需要分配内存时您将调用该接口 - 这对您来说实施起来很痛苦,无论如何我可能只是将它传递给 operator new 。您能做的最糟糕的事情是“最了解”并创建您自己的自定义堆。如果内存分配性能是一个问题,我宁愿你分享整个游戏使用的解决方案,而不是你自己的。

于 2013-06-25T21:35:45.263 回答
3

如果您想编写自己的 malloc()/free() 等,您可能应该首先查看现有系统的源代码,例如dlmalloc。然而,这是一个难题,因为它的价值。编写自己的 malloc 库很难。击败现有的通用 malloc 库将更加困难。

于 2013-06-25T20:08:55.360 回答
3

现在,这是正确的答案:不要再实现另一个内存管理器。

实现一个不会在不同类型的使用模式和事件下失败的内存管理器是非常困难的。您可能能够构建一个在您的使用模式下运行良好的特定管理器,但编写一个对许多用户运行良好的管理器是一项几乎没有人真正做得好的全职工作。更糟糕的是,实现一个内存管理器非常容易,它在 99% 的时间里都能很好地工作,然后在 1% 的时间里由于意外的堆碎片而崩溃或突然消耗系统上的大部分或全部可用内存。

我这样说是作为一个写过多个内存管理器的人,看着很多人编写自己的内存管理器,并且看到更多的人尝试编写内存管理器并失败了。这个问题看似困难,不是因为很难编写模板分配器和具有继承等的泛型类型,而是因为该线程中给出的其他解决方案往往会在加载行为的角落类型下失败。一旦你开始支持字节对齐(所有现实世界的分配器都必须),那么堆碎片就会出现它丑陋的头。可爱的启发式算法适用于小型测试程序,但在遇到大型真实世界程序时会惨遭失败。

一旦你让它工作,其他人将需要: cookie 来验证内存踩踏;堆使用报告;内存池;池池;内存泄漏跟踪和报告;堆审计;块拆分和合并;线程本地存储;旁观者;CPU 和进程级别的页面错误和保护;设置、检查和清除“空闲内存”模式又名 0xdeadbeef;以及其他我无法想到的事情。

编写另一个内存管理器完全属于过早优化的标题。由于有多个免费的、良好的内存管理器,它们背后有数千小时的开发和测试,你必须证明花费你自己的时间成本是合理的,这样结果会比其他人提供某种可衡量的改进已经完成,您可以免费使用。

如果您确定要实现自己的内存管理器(希望您在阅读此消息后不确定),请详细阅读 dlmalloc 源代码,然后详细阅读 tcmalloc 源代码,然后确保您了解实现线程安全与线程不安全内存管理器的性能权衡,以及为什么幼稚的实现往往会产生较差的性能结果。

于 2014-01-21T05:38:26.010 回答
2
  1. 准备多个解决方案并让框架的用户采用任何特定的解决方案。您开发的通用分配器的策略类会很好地做到这一点。

  2. 解决这个问题的一个好方法是用重载的 * 运算符将指针包装在一个类中。使该类的内部数据仅作为内存池的索引。现在,您可以在后台线程复制数据后快速更改索引。

  3. 大多数goodC++ 库都支持分配器,您应该实现一个。您还可以重载全局 new 以便使用您的版本。请记住,您通常不需要考虑分配或释放大量数据的库,这通常是客户端代码的责任。

于 2013-06-25T20:14:17.677 回答