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

时间:2012-03-21 13:59:51

标签: c++ memory-management collections containers

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

我最近在一些细节上读了一本描述内存碎片(在堆上)的书,它让我想到了我自己的项目。例如,以下列方式使用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]

感谢您的回答/评论!

3 个答案:

答案 0 :(得分:2)

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

内存分配

当您调用operator new(默认情况下通常会在后台调用malloc)时,您不会直接从操作系统请求内存,而是(通常)会发生以下情况:

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

内存碎片可能发生在两个层面:

  • 您可能耗尽虚拟地址空间(使用64位平台时不那么容易)
  • 您可能有几乎空白的页面无法回收,但无法满足您的要求

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

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

典型策略

当我说天真时,我指的是你想要不断分配所有东西的想法。这是一个坏主意。这通常会发生什么。

相反,今天好的malloc实现使用了池。

通常,他们将拥有每个尺寸的

  • 1个字节
  • 2个字节
  • 4个字节
  • ...
  • 512字节
  • ...
  • 4KB及以上(特别是直接)处理

当你提出请求时,他们会找到满足它的最小尺寸的游泳池,这个游泳池将为你服务。

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

那么,碎片?

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

但是你仍然可以观察到记忆孔。假设一个池正在处理9到16个字节的对象,你可以分配4,000,000个对象。这需要至少16,000页的4KB。现在假设您释放了除了16,000个之外的所有内容,但是狡猾地使每个页面仍然存在一个对象。操作系统无法回收页面,因为您仍然使用它们,但是由于您只使用了4KB中的16个字节,因此空间非常浪费(目前)。

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

魔术容器

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

<强> TL; DR

不要担心碎片。

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

答案 1 :(得分:0)

(编辑:抱歉,误读了您的问题;之前的答案已删除。)

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

编辑:似乎需要一个例子。警告:这是完全未经测试的,从我的头顶。不要指望它编译。<​​/ p>

#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大得多也行。它也不依赖于自动释放池是恕我直言的危险。内存布局不紧,因为池总是会分​​配,但它没有碎片,如果我理解你的问题,这就是你想要的。

答案 2 :(得分:0)

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

如果要计算内存碎片,可以重载对象operator new并实现自己的内存管理系统。我建议你阅读memory poolsfree lists的主题。