一个大泳池还是几个特定类型的泳池?
我正在开发一款需要高性能的视频游戏,因此我尝试设置良好的内存策略或游戏的特定部分,即游戏“模型”、游戏表示的部分。
我有一个包含整个游戏表示的对象,内部有不同的管理器以保持表示一致,遵循游戏规则。每个游戏实体当前都是由特定于类型的工厂生成的,因此我有几个工厂,可以让我根据需要隔离和更改这些实体的内存管理。
现在,我正在这两种选择之间进行选择:
- 为每种类型提供一个内存池:这将允许真正快速的分配/释放和最小的碎片,因为对象池已经知道了大小分配的对象。困扰我的一件事是拥有多个像这样的独立池,也许会使其他解决方案更有效...
- 让一个游戏表示的所有工厂共享一个大内存池:(使用某种东西就像带有一些适配器函数的 boost::pool 一样),这样我就可以将所有游戏对象内存分配在一起,并且可以为我已经知道总大小的游戏分配一位(情况并非总是如此)。我不确定它是比 A 更好的解决方案,因为池内可能存在碎片,因为同一池中可能存在不同大小的对象,但它看起来对于内存分析和其他问题修复来说更容易。
现在,我对 A 有一些现实世界的经验,所以我对 B 没有经验,并且想要一些关于这些解决方案的建议,以实现长期项目。 对于一个长期项目来说,哪种解决方案似乎更好?为什么?(注意:在这种情况下,池确实是必要的,因为游戏模型也用于游戏编辑,所以会有大量的分配/解除分配小物体)。
编辑澄清:我正在使用 C++ if (还不清楚)
I'm working on a video game which requires high performance so I'm trying to setup a good memory strategy or a specific part of the game, the part that is the game "model", the game representation.
I have an object containing a whole game representation, with different managers inside to keep the representation consistent, following the game rules. Every game entity is currently generated by a type-specific factory, so I have several factories that allow me to isolate and change the memory management of those entities as I wish.
Now, I'm in the process of choosing between those two alternatives:
- Having a memory pool for each type: that will allow really fast allocation/deallocation and minimal fragmentation as an object pool already know the size of the allocated objects. One thing that bother me is to have several pools like that that are separate, maybe making the other solution more efficient...
- Having one big memory pool shared by all factories of one game representation : (using something like boost::pool with some adapter functions) that way I've got all the game objects memory allocated together and can have one bit allocation for a game that I already know the total size (it's not always the case). I'm not sure it's a better solution than A because of possible fragmentation inside the pool, as there would be objects of different size in the same pool, but it looks like an easier one for memory analysis and other problem fixing.
Now, I had some real worlds experiences with the A so I'm not experienced with B and would like some advice regarding those solutions, for a long-life project. Which solution seem better for a long-life project and why? (Note: a pool is really necessary in this case because the game model is used for game editing too so there will be lot of allocation/deallocation of little objects).
Edit for clarification: I'm using C++ if (it's not clear yet)
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(6)
正确答案特定于您的问题域。但在我工作的问题领域中,我们通常选择第一个。
我编写实时或接近实时的代码。主要是音频编辑和播放。在该代码中,我们通常无法在播放引擎中从堆向下分配内存。大多数时候 malloc 返回得足够快,但有时则不然。有时这很重要。
因此,我们的解决方案是为某些对象使用特定的池,并为其他所有对象使用通用池。特定的池预先分配了一定数量的元素,并以链表(实际上是队列)的形式实现,因此分配和释放永远不会超过几次指针更新以及进入和离开临界区的成本。
作为异常情况的后备措施;当有人需要从特殊池中分配并且它是空的时 - 我们将分配一大块通用内存(几个对象)并将其添加到特殊池中。一旦分配成为特殊池的一部分,它就永远不会返回到通用池,直到应用程序退出或启动新项目。
对特殊池的初始大小和最大大小做出正确的选择是调整应用程序的重要部分。
The correct answer is specific to your problem domain. But in the problem domains that I work, the first is usually the one we choose.
I do realtime or near realtime code. Audio editing and playback mostly. In in that code, we generally cannot afford to allocate memory from the heap down in the playback engine. Most of the time malloc returns fast enough, but sometimes it doesn't. And that sometimes matters.
So our solutions is to have specific pools for certain objects, and use the general pool for everything else. The specific pools have a certain number of elements preallocated, and are implemented as a linked list (actually a queue), so allocation and release is never more than a couple of pointer updates and the cost of entering and leaving a critical section.
As a fallback for unusual cases; when someone needs to allocate from a special pool and it's empty - we will allocate a hunk of general memory (several objects) and add that to the special pool. Once an allocation is part of the special pool, it is NEVER returned to the general pool until the app exits or starts a new project.
Making good choices about the initial size and maximum size of the special pools are an important part of tuning the application.
您将遇到的一个问题是,STL 实现可以假设同一类型的两个分配器是等效的。这就是 Boost.Pool 仅使用一个池的原因(从技术上讲,它为每种类型使用不同的池)。 IE,在一般情况下,您的分配器不允许有任何非静态成员。如果您正在制作视频游戏并且知道您的 STL 实现不存在此问题,那么不必担心这一点 - 但是
list::splice
和容器上的std::swap
。One problem that you'll run into is that STL implementations are allowed to assume that two allocators of the same type are equivalent. This is the reason that Boost.Pool uses only one pool (technically it uses a different pool for each type). I.E., your allocators are not allowed to have any non-static members in the general case. If you're making a video game and you know that your STL implementation does not have this problem, then don't worry about this -- however there might still be some issues with
list::splice
andstd::swap
on containers.对于初学者来说,对于任何类型的视频游戏使用 stl 或 boost 都是不切实际的。你可以绝对确定,当你使用一个 stl 容器时,你的内存已经支离破碎,而且你在厕所里的表现至少与理想状态相比是无可救药的(因为大多数人的代码都属于这一类,大多数人从来没有注意到,也无法真正与它进行比较)还要别的吗)。我并不总是这么强烈地思考,但随着时间的推移,我发现即使是几行代码也像一个小精灵,最终有一天会给你带来巨大的痛苦。
第一种方法是最常见的,对于同时完成这两种方法的人来说,如果您不想在问题上花费比您可能值得的更多时间和精力,那么这可能是唯一实用的方法。第二种方法更好,因为它更通用,但可以根据您的具体需求进行定制,但它需要大量工作,不能轻易投入。
It's not practical to use stl or boost for any type of video game, for starters. You can be absolutely sure the second you use one stl container your memory is fragmented and your performance is hopelessly in the toilet compared to the ideal at least (since most everyone's code is in this category most people never notice and can't really compare to anything else). I didn't always think so strongly but over time I have seen even a couple lines of code is like a little gremlin that will eventually some day cause you great pain.
The first method is most common, and as someone who's done both it's probably the only way that's practical if you don't want to spend a lot lot LOT more time and energy on the problem than it's probably worth to you. The second way is better because it's more general and yet can be tailored to your exact needs, but it's a lot of work and not something to jump into lightly.
一种可能的解决方案是介于 1 和 2 之间。
对小对象使用池:每个对象大小一个池。在这种情况下,您可以通过将指针存储在数组中来轻松找到池。
此外,您还可以拥有一个用于存放大型对象的池。在这种情况下,碎片的可能性较小,时间开销也不是那么重要,因为大对象不会非常频繁地分配和释放。
请注意
boost::pool
。在测试boost::pool
的性能时,不仅要测试分配,还要测试释放。我经历过boost::pool
和boost::fast_pool
释放时间可能非常长。我的案例包括在一个池中分配和取消分配不同大小的小对象One of the possible solution is something between 1. and 2.
Use pools for small objects: one pool per object size. In this case you can easy find pool by storing pointers in array.
And in addition you can have one pool for large objects. In this case fragmentation is less probable and time overhead is not so critical because large objects are not allocated and deallocated very frequently.
Note about
boost::pool
. When testing performance ofboost::pool
, test not only allocation but also deallocation. I experienced thatboost::pool
andboost::fast_pool
deallocation time can be extremally large. My case consisted of allocations and de-allocations of small objects of different sizes in one pool我对您正在考虑的内存管理器没有具体的经验,但这里有一些可能有帮助的一般准则:
通过使用多个池进行开发,但使用单个池进行最终测试和生产发布,您可能会获得两全其美的效果(假设速度相似)。这样,您可以在开发过程中发现分配/管理问题,但仍然可以从潜在更高效的单一池中受益。
I don't have specific experience with the memory manager you are considering, but here are some general guidelines that may help:
You might get the best of both worlds (assuming speed is similar) by developing with multiple pools, but doing final testing and the production release with a single pool. That way, you can spot allocation/management issues during development but still benefit from the potentially more efficient single pool.
实际上,我会选择 2。我可以给你一个 Linux 内核的例子。在内核中,dentry(目录项)和inode对象应该在内存中缓存更长的时间,以便更好地响应用户。由于 inode 对象依赖于文件系统,因此每个文件系统都会创建自己的对象池。如果对象相似,您还可以做的另一件事是抽象出对象并将公共属性保留在一个抽象对象中,并使用容器存储对象特定信息。请参阅下面的代码了解完整的想法。
http://lxr.linux.no/linux+ v2.6.32/fs/ext2/super.c#L149
Actually, I will go with 2. I can give you an example from linux kernel. In kernel, the dentry(directory entry) and inode objects should be cached in memory for longer time for better responsiveness to users. As inode object depends on the file system, each filesystem will create its own pool of objects. One more thing you could do if the objects are similar is to abstract out the objects and keep common attributes in one abstract object and store the object specific information using a container. Refer to the below code for complete idea.
http://lxr.linux.no/linux+v2.6.32/fs/ext2/super.c#L149