C++新内存分配碎片化

4
我正在尝试查看新分配器的行为,以及为什么它不能连续放置数据。
我的代码:
struct ci {
    char c;
    int i;
}

template <typename T>
void memTest()
{
    T * pLast = new T();
    for(int i = 0; i < 20; ++i) {
         T * pNew = new T();
         cout << (pNew - pLast) << " ";
         pLast = pNew;
    }
}

我用char、int和ci运行了这个程序。大多数分配都是从上一个固定长度开始的,有时会从一个可用块跳到另一个可用块。
sizeof(char):1 平均跳跃:64字节
sizeof(int):4 平均跳跃:16
sizeof(ci):8(int必须放在4字节对齐上) 平均跳跃:9
有人能解释一下为什么分配器会像这样分片内存吗?还有为什么char的跳跃要比int和同时包含int和char的结构体要大得多呢?

我应该补充一下,在调试模式和发布模式下,行为非常不同。我认为调试模式中的额外空间是为了调试目的... - Jason T.
我不认为对于通用分配器做出任何假设是值得的——它会因不同模式、平台而异。我并不是要打击你的积极性——如果你对底层工作原理有兴趣,应该绝对继续深入研究!(据此点赞了你的问题) - Matt Curtis
5个回答

10

这里有两个问题:

  • 大多数内存分配器在块开始之前存储一些额外的数据(通常是块大小和一些指针)

  • 通常有对齐要求,现代操作系统通常按至少8字节边界进行分配。

因此,在连续分配之间几乎总会出现某种间隙。

当然,你永远不应该依赖于这种行为的任何特定情况,因为实现可以自由地做任何它想做的事情。


我并不打算依赖于特定的行为。只是想理解分段来自哪里,这样我就知道如果我有内存限制,为什么要进行大块分配并自己分配内存。 - Jason T.
@tamulji:是的,有时候你想要使用单个大的分配并自行划分它。我想到的两种情况是:(i)当你有很多非常小的分配时,(ii)对于像图像处理这样的东西,你希望你的图像是 T **,但你也希望行是连续的。 - Paul R
即使在内存紧张的情况下,或者需要非常快速的分配/释放操作时,如果您对分配模式和生命周期有很多了解 - 如果您有特定要求,您可以始终比通用分配器做得更好,但这种权衡是通用分配器只是“存在”并且易于使用。 - Matt Curtis

6

你的代码存在一个错误,如果想要知道指针之间的距离,请将它们强制转换为(char *)类型,否则差值将以sizeof(T)的大小给出。


是的,我发帖后意识到了这一点。但仍然显示出了碎片化的问题。 - Jason T.
1
在计算差值之前,将其强制转换为整数类型,例如ptrdiff_t或intptr_t。 - Void
对我来说看起来很一致。int和char的分配相差64字节。结构体“ci”相差72字节。你没有说你在哪个操作系统上运行,但似乎分配器将块放在彼此后面。 - plodoc

3

这不是分段,而只是将您的分配大小调整为圆形块大小。

在一般编程中,您不应该关注像new这样的通用分配器返回的内存地址模式。当您关心分配行为时,应始终使用特定目的的分配器(例如boost::pool,自己编写的分配器等)。

例外情况是您正在研究分配器,在这种情况下,您可以阅读K&R来了解一个简单的分配器,这可能有助于您了解new如何获取其内存。


2
通常情况下,您不能依赖特定的内存位置。内存分配器的内部簿记数据和对齐要求都可以影响块的位置。没有要求块必须连续分配。
此外,有些系统会给您带来更“奇怪”的行为。许多现代Linux系统启用了堆随机化功能,其中新分配的虚拟内存页面被放置在随机地址上,以使某些类型的安全漏洞利用更加困难。使用虚拟内存,不同的分配块地址并不一定意味着物理内存是分段的,因为虚拟地址空间没有要求必须密集。

0

对于小的分配,boost有一个非常简单的分配器,我使用过叫做boost::simple_segregated_storage

它创建了一个自由块和已用块的slist副本,所有大小相同。只要你只分配到其设置的块大小,就不会出现外部碎片(尽管如果你的块大小大于请求的大小,则可能会出现一些内部碎片)。如果以这种方式使用它,它还可以运行O(1)。非常适合模板编程中常见的小分配。


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