9

我正在开发一个使用 C++ 编写的大型服务器应用程序。该服务器可能需要运行数月而不重新启动。碎片在这里已经是一个可疑的问题,因为我们的内存消耗会随着时间的推移而增加。到目前为止,测量一直是将私有字节与虚拟字节进行比较,并分析这两个数字的差异。

我对碎片化的一般方法是将其留给分析。我对一般性能和内存优化等其他事情有相同的思考方式。您必须通过分析和证明来支持更改。

在代码审查或讨论期间,我注意到很多,内存碎片是最先出现的事情之一。这几乎就像现在对它有巨大的恐惧,并且有一个提前“防止碎片化”的大举措。请求的代码更改似乎有利于减少或防止内存碎片问题。我倾向于立即不同意这些,因为它们对我来说似乎是过早的优化。我会牺牲代码的清洁度/可读性/可维护性/等等。为了满足这些变化。

例如,采用以下代码:

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

上面,字符串流在这里进行的分配数量是未定义的,它可能是 4 个分配,或者只是 1 个分配。所以我们不能仅基于此进行优化,但普遍的共识是要么使用固定缓冲区,要么以某种方式修改代码以可能使用更少的分配。我并没有真正看到 stringstream 在这里扩展自身是导致内存问题的巨大贡献者,但也许我错了。

对上述代码的一般改进建议如下:

std::stringstream s;
s << "This is a string"; // Combine it all to 1 line, supposedly less allocations?

还有一个巨大的推动力是尽可能使用堆而不是堆。

有没有可能以这种方式抢占内存碎片,或者这只是一种错误的安全感?

4

6 回答 6

14

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

然而,更好的方法是使用无碎片的自定义分配器,如对象池或内存区域,它可以保证没有碎片。例如,在物理引擎中,您可以将内存区域用于所有每个滴答分配并在最后清空它,这不仅速度快得离谱(甚至比_allocaVS2010 还要快),而且内存效率极高且碎片率极低。

于 2012-05-17T03:54:40.147 回答
6

在算法层面考虑内存碎片是绝对合理的。在堆栈上分配小的、固定大小的对象也是合理的,以避免不必要的堆分配和释放的成本。但是,我肯定会在任何使代码更难调试、分析或维护的地方划清界限。

我也会担心有很多完全错误的建议。人们通常所说的应该“避免内存碎片”的事情中可能有 1/2 可能没有任何效果,其余的相当大一部分可能是有害的。

对于典型的现代计算硬件上大多数现实的、长时间运行的服务器类型的应用程序,用户空间虚拟内存的碎片不会成为简单、直接的编码的问题。

于 2012-05-17T03:54:26.640 回答
1

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

小优化的问题是为不同但具有相同业务逻辑的东西更改代码。就像使用反向 for 循环一样,因为它比常规 for 更快。您的单元测试可能会指导您优化一些没有副作用的点。

于 2012-05-17T03:52:46.370 回答
1

在你真正遇到内存碎片之前,过多关注它显然是过早的优化;我不会在最初的设计中考虑太多。诸如良好封装之类的事情更重要(因为它们将允许您稍后更改内存表示,如果需要)。

另一方面,避免不必要的分配是好的设计,并尽可能使用局部变量而不是动态分配。不仅是因为碎片化的原因,也是因为程序简单的原因。C++ 通常倾向于使用值语义,使用值语义(复制和赋值)的程序比使用引用语义(动态分配和传递指针)的程序更自然。

于 2012-05-17T07:50:06.600 回答
0

我认为您不应该在真正遇到碎片问题之前解决它,但同时您的软件应该设计为允许轻松集成这样的内存碎片问题解决方案。而且由于解决方案是自定义内存分配器,这意味着应该通过在 config.h 文件中的某处更改一行代码来将一个插入到您的代码中(操作员 new/delete 和容器的分配器类),而不是通过遍历所有容器等的所有实例化。支持这一点的另一点是,当前所有复杂软件中有 99% 是多线程的,来自不同线程的内存分配会导致同步问题,有时还会导致错误共享。而这些问题的答案又是自定义内存分配器。

因此,如果您的设计支持自定义分配器,那么您不应该接受以“碎片释放”形式出售给您的代码更改,直到您分析您的应用程序并亲眼看到该补丁确实减少了 DTLB 或 LLC 未命中的数量通过更好地打包数据。但是,如果设计不允许自定义分配器,那么这应该作为第一步在进行任何其他“内存碎片消除”代码更改之前实现。

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

另一个小点:您使用字符串流分配和尽可能将分配打包在一起的策略的示例 - 我的理解是,在某些情况下,这将导致内存碎片而不是解决这个问题。将所有分配打包在一起将使您请求大的连续内存块,最终可能会分散,然后其他类似的大块请求将无法填补空白。

于 2012-05-17T13:18:14.727 回答
-3

还有一点我想提的是:你为什么不尝试某种垃圾收集器。您可以在某个阈值或某个时间段之后调用它。垃圾收集器会在达到一定阈值后自动收集未使用的内存。

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

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

因此,您将避免多次调用 malloc 和 new 并且碎片会非常少。代码也将像以前一样可读,因为您只需要实现类似 myAlloc 的东西,它将从您的 cacheA、cacheB 等中分配...

于 2012-05-17T05:49:15.337 回答