背景:我正在开发一种多平台框架,它将用作游戏和 util / tool 创建的基础。基本思想是拥有一个工作池,每个工作线都在自己的线程中执行。 (此外,工作人员也可以在运行时生成。)每个线程都有自己的内存管理器。
我一直在考虑创建自己的内存管理系统,我认为这个项目最终会尝试一下。我发现这种系统适合由于这个框架的用法类型通常需要实时内存分配(游戏和纹理编辑工具)。
问题:
没有普遍适用的解决方案(?) - 该框架将用于游戏/可视化(不是AAA,而是独立/播放)和工具/应用程序创建。我的理解是,对于游戏开发,通常(至少对于控制台游戏)在初始化时只分配一大块内存,然后在内存管理器内部使用这个内存。但这种技术是否适用于更普遍的应用?
在游戏中你理论上你可以知道你的场景和资源需要多少内存,但是例如,照片编辑应用程序将加载所有不同大小的资源......所以在后一种情况下,一个更动态的内存“块大小“需要吗?这让我想到了下一个问题:
移动已分配的数据并保留有效指针 - 通常在堆上分配时,您将获得一个指向内存块的简单指针。在自定义内存管理器中,据我所知,类似的方法是返回指向预分配块中空闲区域的指针。但是,如果预先分配的块太小而需要调整大小甚至进行碎片整理,会发生什么?数据需要在内存中移动,旧指针无效。有没有办法以某种方式透明地包装这些指针,但仍然使用它们作为通常“在内部”的内存管理,就好像它们是通常的C ++指针一样?
第三方库 - 如果无法透明地将自定义内存管理系统用于应用程序中的所有内存分配,那么我正在链接的每个第三方库仍然会在内部使用“旧的”OS内存分配。我已经了解到库通常会公开函数来设置库将使用的自定义分配函数,但不保证我将使用的每个库都具有此功能。
问题:实现可以使用动态大小的内存块池的内存管理器是否可行且可行?如果是这样,如何在不破坏当前正在使用的指针的情况下碎片整理和内存调整大小如何工作?最后,如何最好地实现这样的系统与第三方库一起工作?
我也很感谢任何相关的阅读材料,论文,文章等等! : - )
答案 0 :(得分:9)
作为前几代游戏机为AAA游戏编写了许多内存管理器和堆实现的人,让我告诉你它根本不值得。
你的信息已经过时了 - 回到游戏立方时代[大约2003年]我们曾经做过你所说的 - 分配一大块并使用为每个游戏调整的自定义算法手动分割出这个块。
一旦虚拟内存出现(xbox时代),游戏变得更加复杂[并因此进行了更多的分配并变成了多线程]地址碎片使得这种情况变得难以为继。因此我们切换到自定义分配器来处理某些类型的请求 - 例如物理内存,或锁定免费的小块低碎片堆或最近使用的块的线程本地缓存。
随着内置内存管理器变得越来越好,它比那些更难做得更好 - 当然在一般情况下,对于特定用例来说更接近。 Doug Lea Allocator [或者现在主流的c ++ linux编译器]以及最新的Windows低碎片堆非常好,你可以更好地将时间投入其他地方。
我已经在工作中使用电子表格来衡量一大堆分配器的各种指标 - 所有大名鼎鼎的,以及我多年来收集的一些。基本上虽然专业分配器可以在一些指标上获胜[每个分配的最低开销,空间接近度,最低碎片率等],但总体指标却是最好的。
作为您图书馆的用户,我个人首选的选项是您只需在需要时分配内存。使用operator new
/ new
运算符,我可以使用标准C ++机制来替换它们并使用我的自定义堆(如果我确实有),或者我可以使用特定于平台的方法来替换您的分配(例如Xbox上的XMemAlloc)。我不需要标记[捕获callstacks远远优于我可以做的,如果我想要的话]。降低该列表会给你一个接口,当你需要分配内存时,你会给它打电话 - 这只是你实施的一个难题,我可能只是将它传递给operator new。你能做的最糟糕的事情就是“最了解”并创建自己的自定义堆。如果内存分配性能有问题,我宁愿你分享整个游戏使用的解决方案,而不是自己动手。
答案 1 :(得分:3)
如果您正在编写自己的malloc()/ free()等,您可能应该首先查看现有系统的源代码,例如dlmalloc。然而,这是一个很难的问题,因为它的价值。编写自己的malloc库是很难的。击败现有的通用malloc库将是Even Harder。
答案 2 :(得分:3)
现在,这是正确的答案:不要执行另一个内存管理器。
实现一个在不同使用模式和事件下不会失败的内存管理器是非常困难的。您可能能够构建一个在您的使用模式下运行良好的特定管理器,但是编写一个适用于许多用户的管理器是一项几乎没有人真正做得很好的全职工作。更糟糕的是,实现内存管理器非常容易,99%的时间内工作得很好,然后1%的时间会因为意外的堆碎片而崩溃或突然消耗系统中的大部分或全部可用内存。
我说这是一个写过多个内存管理器的人,看着多个人编写自己的内存管理器,看着更多人尝试编写内存管理器并失败。这个问题看起来很困难,不是因为很难用继承来编写模板化的分配器和泛型类型,而是因为这个线程中给出的其他解决方案往往会在角落类型的加载行为下失败。一旦你开始支持字节对齐(因为所有真实世界的分配器必须),那么堆碎片就会变得丑陋。可爱的启发式方法非常适合小型测试程序,在受到大型真实世界程序的影响时会失败。
一旦你开始工作,别人就需要:用来验证内存踩踏的cookie;堆使用报告;内存池;游泳池;内存泄漏跟踪和报告;堆审计;大块分裂和合并;线程本地存储; lookasides; CPU和进程级页面错误和保护;设置和检查并清除“自由内存”模式,即0xdeadbeef;还有我无法想到的其他任何事情。
写另一个内存管理器完全属于早熟优化的标题。由于有多个自由,优秀的内存管理员,他们背后有数千小时的开发和测试,你必须证明花费你自己时间的成本是合理的,结果会比其他人提供某种可衡量的改进。已经完成,你可以免费使用。
如果您确定要实现自己的内存管理器(希望您在阅读此消息后不确定),请详细阅读dlmalloc源代码,然后详细阅读tcmalloc源代码,然后确保您了解实现线程安全与线程不安全内存管理器的性能权衡,以及为什么天真实现往往会导致性能不佳的结果。
答案 3 :(得分:2)
准备多个解决方案,让框架的用户采用任何特定的解决方案。您开发的通用分配器的策略类可以很好地完成。
解决这个问题的一个好方法是用一个带有重载*运算符的类来包装指针。使该类的内部数据只是内存池的索引。现在,您可以在后台线程复制数据后快速更改索引。
大多数good
C ++库支持分配器,您应该实现一个。您还可以重载全局new,以便使用您的版本。请记住,您通常不需要考虑分配或取消分配大量数据的库,这通常是客户端代码的责任。