防止多态容器中的内存碎片

Preventing Memory Fragmentation in Polymorphic Container

本文关键字:内存碎片 多态      更新时间:2023-10-16

这个问题需要一些解释,所以如果你不跳过这个例子,请竖起大拇指:)

我最近读了一本书,详细描述了内存碎片(堆上),它让我思考自己的项目。例如,当以以下方式使用ptr_container(来自Boost)时

ptr_array<A> arr;       // 
arr.push_back(new A()); // a1.
arr.push_back(new A()); // a2.
arr.push_back(new A()); // a3.
....

在替换元素时,它将很快导致一些内存碎片。为了便于论证,假设实际的容器可以容纳我们给它的所有指针

[arr_array][a1][a2][a3]...[aN]

当我们开始用一个子类型(具有更大的大小)替换指针时,这种情况发生了变化,假设我们将奇数指针(a1,a3,…)引用的所有对象替换为一个更大的类型,那么它看起来像:

[arr_array][xx][a2][xx][a4]...[aN][b1][b3]...[bN]

其中[xx]表示未使用的空间,b1…bN是新对象。

所以我想要的是一个存储对象的容器(比如STL容器),但支持多态存储。我知道如何实现这种容器,但我更喜欢使用"专家"库(Boost,STL,…)

是否有一个容器支持(动态分配的)保存在连续内存序列中的多态对象

例如,内存布局可能如下所示:

[arr_array] = [ptr_lookup_table][storage]
            = [p1, p2, ..., pn][b1, a2, b3, ..., aN]

感谢您的回答/评论!

内存碎片需要预先了解内存分配,所以我需要先设置一些概念。

内存分配

当您调用运营商new(默认情况下,它通常会在后台调用malloc)时,您不会直接向操作系统请求内存,相反(通常)会发生以下情况:

  • 您调用malloc 76个字节,它看起来是否可用:
    • 如果不是,它会从操作系统请求一个页面(通常为4KB),并准备
  • 然后它会为您提供您要求的76个字节

内存碎片可能发生在两个级别:

  • 你可能会耗尽你的虚拟地址空间(使用64位平台就不那么容易了)
  • 您可能有几乎为空的页面,这些页面无法回收,但无法满足您的请求

一般来说,由于malloc应该一次调用4KB的页面(除非你要求更大的块,在这种情况下,它会选择更大的4KB倍数),所以你永远不应该耗尽你的地址空间。不过,它发生在32位机器(限制为4GB)上,用于异常大的分配。

另一方面,如果malloc的实现过于天真,那么最终可能会出现碎片化的内存块,因此内存占用比实际使用的内存占用要高得多。这通常是术语内存碎片现在所指的。

典型策略

当我说天真时,我指的是,例如,你不断分配一切的想法。这是个坏主意。这通常是而不是发生的情况。

相反,现在优秀的malloc实现使用池。

通常,每个大小都有:

  • 1字节
  • 2字节
  • 4字节
  • 512字节
  • 4KB及以上是专门(直接)处理的

当你提出请求时,他们会找到能满足你的最小大小的池,这个池会为你服务。

因为在一个池中,所有请求都以相同的大小提供服务,所以池中没有碎片,因为空闲单元可以用于任何传入请求。

那么,碎片化

如今,你不应该观察碎片本身。

然而,你仍然可以观察到内存漏洞。假设一个池正在处理9到16字节的对象,并且您分配了4000000个对象。这需要至少16000页4KB。现在假设您解除分配除16000个对象之外的所有对象,但不小心使每个页面仍然存在一个对象。操作系统无法回收这些页面,因为您仍在使用它们,但由于4KB中只使用了16个字节,因此(目前)空间相当浪费。

一些带有垃圾回收的语言可以用压缩来处理这些情况,但是在C++中,由于用户可以直接控制对象地址,因此无法在内存中重新定位对象。

魔术容器

我不知道有这样的野兽。我也不明白为什么它会有用。

TL;DR

不要担心碎片化。

注意:"专家"可能想编写自己的池分配机制,我想提醒他们不要忘记对齐

碎片发生而不是,因为使用了boost容器。当您经常使用newdelete分配和解除分配不同大小的对象时,就会发生这种情况。ptr_array只是存储指向这些分配对象的指针,并且可能不会显著地导致碎片化。

如果您想对抗内存碎片,可以重载对象operator new并实现自己的内存管理系统。我建议你阅读内存池空闲列表的主题。

(编辑:对不起,误解了你的问题;之前的答案已删除。)

您可以为对象使用任何内存池。通常,您将相同(或相似)大小的对象组合在同一个池中。由于您通常必须在池上调用一个特殊的delete函数,因此我建议您使用带有自定义deleter的shared_ptr。然后,您可以将这个shared_ptr与任何您喜欢的标准容器一起使用。

编辑:似乎需要一个例子。警告:这完全是未经测试的,而且是我的想法。不要指望它会编译。

#include <boost/pool.hpp>
#include <memory>
class A;
class B; // B inherits from A
int main() {
  // could be global
  boost::object_pool<A> a_pool;
  boost::object_pool<B> b_pool;
  std::vector<std::shared_ptr<A>> v;
  v.push_back(std::shared_ptr<A>(a_pool.construct(), [&](A*p) { a_pool.free(p); }));
  v.push_back(std::shared_ptr<A>(a_pool.construct(), [&](A*p) { a_pool.free(p); }));
  v.push_back(std::shared_ptr<A>(a_pool.construct(), [&](A*p) { a_pool.free(p); }));
  v[2] = std::shared_ptr<B>(b_pool.construct(), [&](B*p) { b_pool.free(p); });
  return 0;
}

即使B比A大得多,这也会起作用。它也不依赖于自动释放池,这是IMHO的危险。内存布局并不紧张,因为池总是会过度分配,但它不会有碎片,如果我理解你的问题,这就是你想要的。