一个大型池还是几个特定类型的池?

9
我正在开发一款需要高性能的视频游戏,因此我正在尝试为游戏的特定部分设置良好的内存策略或者说是“模型”,即游戏表现形式。我的一个包含整个游戏表现形式的对象中有不同的管理器来保持表现形式的一致性,遵循游戏规则。每个游戏实体当前都是由特定类型的工厂生成的,因此我有几个工厂可以让我隔离和更改这些实体的内存管理。
现在,我正在选择以下两种方案之一:
1. 为每种类型设置一个内存池:这将允许非常快速的分配/释放和最小的碎片化,因为对象池已经知道所分配对象的大小。但是,让我困扰的是要有几个这样的池是分开的,可能会使另一种解决方案更有效...
2. 由一个游戏表现形式的所有工厂共享一个大的内存池(使用类似boost::pool的适配器函数):这样,我就可以将所有游戏对象的内存分配在一起,并且可以为我已经知道总大小的游戏进行一次大的分配(并不总是这种情况)。我不确定它是否比A更好,因为池内可能存在碎片化,因为池中会有不同大小的对象,但这看起来更容易进行内存分析和其他问题的修复。
现在,我已经对A有了一些实际经验,但对B并不熟悉,并希望获得关于这些解决方案的建议,以便长期项目。哪种方案对于长期项目更好,为什么?(注意:在这种情况下,池确实是必要的,因为游戏模型也用于游戏编辑,因此会有许多小对象的分配/释放)。
编辑以澄清:如果还不清楚,我正在使用C++。
6个回答

10

正确答案取决于您的问题领域。但在我工作的问题领域,通常会选择第一个选项。

我做实时或接近实时的代码。主要是音频编辑和播放。在这些代码中,我们通常无法承担从堆中分配内存到播放引擎。大多数情况下malloc返回足够快,但有时不是。而且有时候很重要。

因此,我们的解决方案是针对特定对象具有特定池,并将一般池用于其他所有内容。特定池具有预先分配的某些元素数量,并且被实现为链表(实际上是队列),因此分配和释放永远不超过几个指针更新以及进入和离开临界区的成本。

对于不寻常的情况,当有人需要从特殊池中分配并且它是空的时,我们会分配一块通用内存(多个对象)并将其添加到特殊池中。一旦分配部分属于特殊池,它就永远不会返回到一般池,直到应用程序退出或开始新项目。

为特殊池的初始大小和最大大小做出良好的选择是调整应用程序性能的重要部分。


4

一个问题是,STL实现允许假设相同类型的两个分配器是等价的。这就是为什么Boost.Pool只使用一个池(技术上它为每个类型使用不同的池)的原因。也就是说,在一般情况下,您的分配器不允许有任何非静态成员。如果您正在制作视频游戏,并且知道您的STL实现没有这个问题,则不必担心此问题 - 但是在容器上可能仍然存在某些list::splicestd::swap问题。


4

对于任何类型的视频游戏,使用stl或boost都不是实用的,首先。你可以确定,一旦你使用一个stl容器,你的内存就会被分割,性能比理想情况下要差得多(因为大多数人的代码都属于这个类别,所以大多数人从来没有注意到,也无法与其他任何东西进行比较)。我以前并没有如此强烈的看法,但随着时间的推移,我发现即使只有几行代码,也像一个小精灵,最终总有一天会给你带来巨大的痛苦。

第一种方法是最常见的方法,作为一个既做过stl又做过boost的人,如果你不想花费更多的时间和精力来解决问题,那么这可能是唯一实用的方法。第二种方法更好,因为它更加通用,但可以根据您的确切需求进行调整,但这需要很多工作,不是轻易就能入手的。


对于任何类型的视频游戏来说,使用STL或Boost都不是实用的,首先这样说很奇怪。许多游戏只对十几个对象进行简单、粗略的物理模拟。有些是回合制的,有些是基于文本的。游戏可以用Flash或JavaScript编写并取得成功。肯定有一些好的、受欢迎的游戏使用STL或Boost。并非所有游戏都是“三A级”的。 - mjwach
使用STL或boost库制作AAA级视频游戏是完全可行的。这个答案来自2009年,现在已经不适用了。现代C++库(它并不等同于术语“STL”)非常高效,只有极端谨慎、极具才华或好奇心者才会想要自己编写专门针对视频游戏需求定制的库。 - KeyC0de

2

可能的解决方案之一是介于1和2之间的某个东西。

对于小对象,请使用池:每个对象大小一个池。在这种情况下,您可以通过在数组中存储指针轻松找到池。

此外,您还可以拥有一个大对象池。在这种情况下,碎片化的可能性较小,时间开销也不是很关键,因为不会经常分配和释放大对象。

关于boost::pool的注意事项。在测试boost::pool的性能时,不仅要测试分配,还要测试释放。我发现boost::poolboost::fast_pool的释放时间可能非常长。我的情况包括在一个池中分配和释放不同大小的小对象。


0

我对你考虑的内存管理器没有具体的经验,但是这里有一些通用的指导方针可能会有所帮助:

  • 如果你不期望出现内存短缺,选项1是最好的选择,因为正如你所说,它很快(比2更快?),并且拥有单独的内存池将使得更容易发现分配/释放/缓冲区问题(假设内存池管理器具有良好的错误检测能力)。
  • 如果内存可能成为一个问题(例如,你的游戏将占用比目标平台常用内存更多的内存),那么使用一个大的内存池将提供更有效的内存使用。此外,如果你无法准确预测每个内存池的平均和最大内存需求,除非内存管理器可以动态增长内存池(最好也可以动态释放内存池中的块),否则这是一个更好的选择。我唯一看到2的缺点是它可能会更慢(真的吗?),而且内存管理中的错误可能更难以检测。

你可以在开发过程中使用多个内存池来获得最佳效果(假设速度相似),但在最终测试和生产发布时使用单个内存池。这样,你可以在开发过程中发现分配/管理问题,但仍然可以从潜在的更有效的单个内存池中受益。


当你说第二种情况可能会更慢时,你并不是指如果我每秒只有1次或更少的分配/释放操作,它就会变慢了吧?(在一个不错的硬件上)你是在谈论每秒大量的分配/释放操作吗? - Klaim
我的意思只是,如果它使用不同的内存管理策略,或者如果它使用相同的策略但在一个池中管理所有对象时该策略变得明显较慢,那么2.可能会更慢。如果您预计每秒进行1次分配,则非常可能存在令人担忧的速度差异。 - Eric J.

0

实际上,我会选择2。我可以给你一个来自Linux内核的例子。在内核中,dentry(目录项)和inode对象应该在内存中缓存更长时间,以便更好地响应用户。由于inode对象取决于文件系统,每个文件系统都将创建自己的对象池。如果这些对象相似,您还可以将对象抽象出来,并将共同属性存储在一个抽象对象中,并使用容器存储特定于对象的信息。请参考下面的代码以获取完整的想法。

http://lxr.linux.no/linux+v2.6.32/fs/ext2/super.c#L149


我认为你实际上是想选择1吧?因为你给出的例子实际上是选项1,而你并没有批评它。 - Gui Prá

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接