13

假设我有一个带有构造函数的内存池对象,该构造函数采用指向一大块内存 ptr 和大小 N 的指针。如果我执行许多不同大小的随机分配和释放,我可以让内存处于无法分配的状态M 字节对象连续在内存中,即使可能有很多空闲!同时,我无法压缩内存,因为这会导致消费者指针悬空。在这种情况下如何解决碎片?

4

5 回答 5

8

我想加我的 2 美分只是因为没有人指出从你的描述中听起来你正在实现一个标准的堆分配器(即我们每次调用 malloc() 或 operator new 时都已经使用的)。

堆正是这样一个对象,它进入虚拟内存管理器并请求大块内存(你称之为“池”)。然后它有各种不同的算法来处理分配各种大小的块并释放它们的最有效方法。此外,多年来,许多人已经修改和优化了这些算法。长期以来,Windows 都带有一个称为低碎片堆 (LFH) 的选项,您过去必须手动启用该选项。从 Vista 开始,LFH 默认用于所有堆。

堆并不完美,如果使用不当,它们肯定会导致性能下降。由于操作系统供应商不可能预测您将使用堆的所有场景,因此必须针对“平均”使用优化他们的堆管理器。但是,如果您的要求类似于常规堆的要求(即许多对象,不同的大小......),您应该考虑只使用堆而不是重新发明它,因为您的实现可能不如操作系统已经为您提供。

使用内存分配,您可以通过放弃一些其他方面(分配开销、分配生命周期......)而不是简单地使用堆来获得性能,这对您的特定应用程序并不重要。

例如,在我们的应用程序中,我们需要许多小于 1KB 的分配,但这些分配只使用了非常短的时间(毫秒)。为了优化应用程序,我使用了 Boost Pool 库,但对其进行了扩展,以便我的“分配器”实际上包含一组 boost pool 对象,每个对象负责分配一个特定大小,从 16 字节到 1024 字节(以 4 为步长)。这提供了几乎免费(O(1) 复杂度)分配/释放这些对象,但问题是 a) 内存使用量总是很大并且即使我们没有分配单个对象也永远不会下降,b) Boost Pool 永远不会释放它使用的内存(至少在我们使用它的模式下),所以我们只将它用于不会停留很长时间的对象。

那么你愿意在你的应用程序中放弃正常内存分配的哪些方面?

于 2011-10-23T06:03:27.017 回答
6

根据系统的不同,有几种方法可以做到这一点。

首先尽量避免碎片化,如果你以 2 的幂次方分配块,那么导致这种碎片化的机会就会更小。还有其他几种方法可以解决它,但是如果您达到这种状态,那么您此时就OOM,因为除了杀死请求内存的进程,阻塞直到可以分配内存或返回 NULL 作为您的分配区域。

另一种方法是将指针传递给数据指针(例如:int **)。然后您可以重新排列程序下方的内存(我希望是线程安全的)并压缩分配,以便您可以分配新块并仍然保留旧块中的数据(一旦系统进入这种状态,虽然这会成为一个沉重的开销,但应该很少做完了)。

还有一些“分箱”内存的方法,以便您拥有连续的页面,例如,仅将 1 页专用于 512 及以下的分配,另一个用于 1024 及以下的分配,等等......这使得决定使用哪个 bin 变得更容易在最坏的情况下,您从下一个最高的 bin 拆分或从较低的 bin 合并,这减少了跨多个页面的碎片的机会。

于 2011-10-22T04:54:11.160 回答
3

为您经常分配的对象实现对象池将大大减少碎片,而无需更改内存分配器。

于 2011-10-22T05:02:19.353 回答
1

更准确地了解您实际尝试做什么会很有帮助,因为有很多方法可以解决这个问题。
但是,第一个问题是:这真的发生了,还是理论上的问题?

要记住的一件事是,您通常拥有比物理内存更多的可用虚拟内存地址空间,因此即使物理内存碎片化,仍然有大量连续的虚拟内存。(当然,物理内存在下面是不连续的,但您的代码没有看到。)

我认为有时存在对内存碎片的无根据的恐惧,因此人们编写了一个自定义内存分配器(或者更糟糕的是,他们编造了一个带有句柄和可移动内存和压缩的方案)。我认为这些在实践中很少需要,有时将其丢弃并重新使用 malloc 可以提高性能。

于 2011-10-22T05:17:40.117 回答
0
  • 编写池以作为分配列表操作,然后可以根据需要扩展和销毁。这可以减少碎片。
  • 和/或实施分配转移(或移动)支持,以便您可以压缩活动分配。对象/持有者可能需要帮助您,因为池可能不一定知道如何传输类型本身。如果池与集合类型一起使用,那么完成压缩/传输要容易得多。
于 2011-10-22T04:57:44.060 回答