在编写代码时考虑内存碎片化:是过早优化还是不是?

9

我正在开发一个使用C++编写的大型服务器应用程序。这个服务器需要长时间运行而无需重新启动。由于我们的内存消耗随着时间的推移而增加,因此碎片化已经成为了一个疑点。到目前为止,我们的测量方法是比较私有字节和虚拟字节,并分析这两个数字之间的差异。

我的处理碎片化的一般方法是留待分析。对于其他类似整体性能和内存优化的问题,我也有同样的思考方式。你必须用分析和证明来支持变化。

我在代码审查或讨论期间注意到了很多关于内存碎片化的事情。似乎现在人们非常害怕它,很大程度上会提前采取“预防碎片化”的措施。他们要求进行代码更改,以减少或预防内存碎片化问题。但我对这些改变持不同意见,因为它们对我来说似乎是过早的优化。为了满足这些更改,我将牺牲代码的清洁度/可读性/可维护性等等。

例如,看下面的代码:

std::stringstream s;
s << "This" << "Is" << "a" << "string";

在这里,stringstream所做的分配数量是未定义的,可能会有4个分配,也可能只有1个分配。因此,我们不能仅根据此进行优化,但一般共识是要么使用固定缓冲区,要么以某种方式修改代码以潜在地减少分配。我不认为stringstream在这里自己扩展会对内存问题产生很大影响,但也许我错了。
对上面的代码的一般改进建议如下:
std::stringstream s;
s << "This is a string"; // Combine it all to 1 line, supposedly less allocations?

现在有一个巨大的推动力量,尽可能地使用堆栈而不是堆。

这种方式能否预防内存碎片化,还是只是一种虚假的安全感?


4
我认为一个简单的判断方法是:在程序中,你需要关注两件事情:实现的正确性和实现的效率。我们都希望这两个方面都能达到最高水平,但实际上,专注于正确性比专注于效率更好,因为世界上最有效率的错误程序仍然是错误的,也是无用的。你应该尽力专注于正确性和效率;过早优化只意味着更加专注于效率而非正确性。如果你有能力在不牺牲正确性的情况下使程序更加高效,那么你肯定应该这样做! - GManNickG
3
这里存在内存碎片化的问题,因为我们的内存消耗会随着时间增加而增加。在我看来,很可能更像是普通的内存泄漏 - 你可能需要先排除这些问题(使用诸如valgrind或drmemory这样的内存泄漏检测工具要比仔细查找整个代码库中引起碎片化的原因容易得多)。 - smocking
6个回答

14

如果你事先知道需要低碎片化,已经预先测量了碎片化是一个实际的问题,并且事先知道哪些代码片段相关,那么进行优化并不会过早。性能是一种要求,但是在任何情况下盲目优化都是不好的。

然而,更优秀的方法是使用无碎片的自定义分配器,例如对象池或内存区域,它们可以保证没有碎片。例如,在物理引擎中,您可以使用内存区域来处理所有每个tick的分配,并在结束时将其清空,这不仅异常快速(甚至比VS2010上的_alloca更快),而且非常节省内存且低碎片化。


作为游戏开发者,自定义分配器和内存池非常常见,并且我们通常从一开始就构建它们。既然知道你需要它们,就没有理由不这样做。这也使得确定各个子系统预算时更加容易。 - Retired Ninja
这里的普遍共识是,因为我们相信分段已经成为一个问题了,因此未来的分配会加剧这个问题。在这方面,他们觉得优化并不是“盲目”的,然而我认为每种情况都具有上下文特定性,存在分段问题可能与未来的分配无关。你有什么想法? - void.pointer
@RobertDailey:你仍然需要确定哪些分配实际上导致了碎片化。没有理由相信随机的额外分配X会有任何影响——就像如果你添加一个随机函数X并调用它一次,即使你已经CPU绑定,它也是无关紧要的。 - Puppy

6
在算法级别考虑内存碎片化是完全合理的。为了避免不必要的堆分配和释放成本,将小的、固定大小的对象分配在栈上也是合理的。然而,我绝对会在任何使代码更难调试、分析或维护的事情上划清界限。
我还担心有很多建议是完全错误的。人们通常说应该做的“避免内存碎片化”的东西中,可能有一半根本没有任何效果,其余的一部分可能会有害。
对于大多数现实的、长时间运行的服务器类型应用程序,在典型的现代计算硬件上,用户空间虚拟内存的碎片化不会是一个问题,只需简单、直接的编码即可。

你的第二条评论非常重要。导致碎片化的原因并不总是显而易见,解决方案也不是。 - edA-qa mort-ora-y

1

我认为这不仅是一种最佳实践,而且也不是过早优化。如果您有一个测试套件,可以创建一组内存测试来运行和测量内存、性能等,例如在夜间时段。您可以阅读报告并尽可能修复一些错误。

小优化的问题在于改变代码以得到不同但具有相同业务逻辑的结果。例如使用反向循环比常规循环更快。您的单元测试可能会指导您优化某些点而没有副作用。


1

过早地关注内存碎片化问题是明显的过度优化;在初始设计中不必过于考虑。像良好的封装这样的东西更重要(因为它们将允许您稍后更改内存表示,如果需要的话)。

另一方面,避免不必要的分配,并在可能时使用局部变量而不是动态分配,是良好的设计。这不仅是为了避免碎片化,还为了程序的简单性。C++通常更喜欢值语义,使用值语义(复制和赋值)的程序比使用引用语义(动态分配和传递指针)的程序更自然。


0

我认为在实际遇到内存碎片问题之前,你不应该解决内存碎片问题,但同时你的软件应该被设计成可以轻松集成解决内存碎片问题的方案。由于解决方案是自定义内存分配器,这意味着将其插入到您的代码中(对于容器使用operator new/delete和分配器类)只需要更改config.h文件中的一行即可,而绝不能通过遍历所有容器的所有实例等方式来完成。支持此观点的另一点是,99%的当前复杂软件都是多线程的,从不同线程分配内存会导致同步问题和有时的虚假共享。这些问题的答案再次是自定义内存分配器。

因此,如果你的设计支持自定义分配器,那么你不应该接受被销售称为“解决内存碎片”代码修改,直到你对你的应用程序进行分析并亲自看到补丁确实通过更好地打包数据减少了DTLB或LLC缺失的数量。然而,如果设计不允许自定义分配器,则在进行任何其他“内存碎片消除”代码更改之前,应首先实施此设置。

根据我对内部设计的记忆,可以尝试使用线程构建块可扩展分配器来同时增加内存分配的可扩展性和减少内存碎片。

另外一个小细节:你用stringstream分配和尽可能地将分配打包在一起的策略所做的示例 - 我的理解是,在某些情况下,这将导致内存碎片化而不是解决此问题。将所有分配打包在一起将使您请求大量连续的内存块,这些内存块可能会散布开来,然后其他类似的大块请求将无法填补间隙。

-3

我还想提一点:为什么不尝试一些垃圾回收器。您可以在某个阈值或某个时间段之后调用它。垃圾回收器将在某个阈值之后自动收集未使用的内存。

此外,关于碎片化问题,请尝试为不同类型的对象分配某种类型的存储,并在代码中管理它们。

例如,如果您有5种类型的对象(A、B、C、D和E类),您可以在开始时在cacheA、cacheB... cacheE中为每种类型分配1000个对象的空间。

这样,您就可以避免许多malloc和new调用,碎片化也会减少。而且由于您只需要实现类似于myAlloc的东西,从cacheA、cacheB等中分配,因此代码仍然很可读。


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