为什么在C++游戏中不要频繁分配和释放内存?

15

我最近转向使用C++进行游戏编程,虽有在C#中处理内存管理和垃圾回收方面的经验,但在C++方面的经验不多。

过去我听到一些模糊的建议,要在游戏过程中避免分配和释放内存(即使用newdelete),并在开始时预先分配所需的所有内容。但这比只在需要时分配和释放游戏对象(敌人、粒子等)更为繁琐和架构复杂。

我认为我读到的建议是针对资源受限平台的 - 我主要是为PC开发,我想会经常变化的游戏状态数据最多只有几兆字节。其余的则是纹理、声音资产等,我将预加载它们。

因此我的问题是:在拥有几千兆字节内存的PC世界中,为了我的游戏状态数据而设置复杂的内存池、预分配等是否值得头疼?或者这只是一种传统的"最佳实践",当最大化有限平台时演变而来,现在被当作信条重复着?

如果我的2MB游戏数据被分散到4MB,我无法想象这对1990年后制造的PC有任何影响 - 但好奇是否有我不知道的东西:)。


6
问题不在于内存碎片化,而是获取内存需要与操作系统进行通信的成本太高了,这很昂贵。无论你有多少内存,这种情况都没有得到缓解。 - GManNickG
我最近开始使用C++进行游戏编程。我为你的处境感到遗憾 ;) 当然只是开个玩笑。 - Mike de Klerk
1
如果您快速频繁地分配和释放内存,尤其是使用不同大小的内存进行分配,则可能会发生内存碎片化。但是在像Windows或Linux这样的操作系统上,在具有一定负载处理能力并且拥有相当大内存的CPU上,除非您注意到性能问题,否则不必过于担心它。我认为更具挑战性的是避免引起内存泄漏! - Mike de Klerk
2
@MikedeKlerk: 最近我居然比用所谓的“简单”的C#更轻松地使用C++ :) - QuadrupleA
1
@QuadrupleA -“最近用C++更容易了…” -那是因为你在C++中有更多的自由和权力 :) - SChepurin
2个回答

23
避免在游戏环境中过度使用new关键字的主要原因有以下两点:
  1. 动态内存分配成本昂贵。
  2. 缓存未命中对性能有损害。
动态内存分配 我们开发类似于游戏的产品(虚拟手术),大部分内存都是预先分配并通过工厂或内存池进行处理。这是因为动态分配内存非常耗费时间。系统必须随时处理多种不同大小的内存请求。这意味着有很多工作需要进行,例如最小化碎片。如果您请求内存,您将需要等待系统执行这些任务。如果您预先分配内存,则可以使用工厂或其他特定于块大小的内存管理器来减轻这些问题。
从我的经验来看,一个简单的错误,例如每帧从头开始分配一个相当大的std::vector而不是重用预先分配的内存,会显著降低帧率。 缓存未命中 另一个相关问题是缓存一致性。缓存未命中,即强制操作系统将新页面载入缓存,也非常昂贵。如果经常发生这种情况,您的游戏将无法玩。然而,如果您预先分配大块内存,则可以大大改善缓存局部性,这使得缓存未命中变得更少。 故事的寓意 总之,如果您不管理自己预先分配的内存,那么您可以预计很多计算时间将浪费在等待系统分配内存或处理缓存未命中上。

感谢您的解释和详细说明。在我的情况下,分配/删除将随着游戏的进展而逐渐进行,而不是每帧和超频繁的操作。我可能会采取“不要事先优化”的策略,如果内存操作成为瓶颈,再考虑预分配。 - QuadrupleA
@QuadrupleA 这听起来非常合理。这也很大程度上取决于游戏的范围。如果你有复杂的碰撞检测、动力学或约束处理,那就是导致这种头疼问题的主要原因。 - Agentlien
@QuadrupleA:虽然这些都是真的,但一个好的动态内存分配器也可以帮助很多。我建议研究一下“hoard”内存分配器。特别是如果你的程序是多线程的。 - Omnifarious
这个答案很棒。我想补充一下,我也会重载new和del运算符,以便使用内存池,这样它就可以在任何项目中使用而不会有太多麻烦。当接手一个死亡的项目时,这非常方便。非常有用的信息性答案。 - Zanven
1
@Zanven 这也是我通常的做法。我认为这是一个很好的解决方案,而且我从Bjarne的《C++设计与演化》中得到了灵感。 - Agentlien

3
在拥有几千兆字节内存的个人电脑世界中,如果运行在PC上的操作系统正常工作,你的进程只有几百兆字节。但这不是主要问题。对于我的游戏状态数据是否值得设置复杂的内存池、预分配等来避免头疼呢?我不敢说“总是预分配一切”,因为有时这是不可能的,而对于不经常使用的对象,可能不值得这样做,但可以肯定的是,C和C++中的动态内存管理需要耗费大量资源并且速度较慢。所以如果你每秒渲染30帧到屏幕上,可能应该尽量避免在每帧/迭代期间分配、释放、再分配用于渲染对象的缓冲区。

1
有趣的是,你知道操作系统分配额外的1K需要多少毫秒吗?我想象的是像物品和敌人这样的东西会随着游戏的进展逐渐累积或释放,而不是每帧都进行(我使用池化构造来处理那种东西)。 - QuadrupleA
1
@QuadrupleA 不要期望得到一个确切的数字数据。 "操作系统" - 是什么操作系统?什么条件?有多少并发线程/进程在运行?硬件的确切规格呢?等等...如果您正在动态分配新敌人的内存,只要每秒钟没有出现100个新角色登场的齐格拉斯冲锋,那应该是可以的。 - user529758
很难准确量化 - 我想我会先采取简单直接的方法,如果它成为瓶颈,那么再考虑池化和预分配。 - QuadrupleA

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