关于c ++:是否有与堆内存分配相关的内存开销(例如,堆中的标记)?

Is there a memory overhead associated with heap memory allocations (eg markers in the heap)?

特别考虑使用最近的Visual Studio C ++编译器的Windows上的C ++,我想知道堆的实现:

假设我使用的是发行版编译器,并且我不关心内存碎片/打包问题,那么与在堆上分配内存相关的内存开销是否存在? 如果是这样,大概每个分配有多少个字节?
64-bit代码中的代码是否大于32-bit

我对现代堆的实现并不太了解,但是想知道是否在每次分配时都将标记写入堆中,或者是否维护了某种表(例如文件分配表)。

在一个相关点上(因为我主要考虑的是诸如" map"之类的标准库功能),Microsoft标准库实现是否曾经使用自己的分配器(用于诸如树节点的分配器)来优化堆使用?


是的,一点没错。

分配的每个内存块都将有恒定的"头"开销,以及较小的可变部分(通常在末尾)。究竟有多少取决于所使用的确切C运行时库。过去,我实验性地发现每个分配约为32-64个字节。可变部分是为了应对对齐方式-每个内存块都将对齐到一些不错的2 ^ n基地址-通常为8或16个字节。

我不熟悉std::map或类似内容的内部设计是如何工作的,但是我非常怀疑它们在那里有特殊的优化。

您可以通过以下方法轻松测试开销:

1
2
3
4
5
6
7
8
char *a, *b;

a = new char;
b = new char;

ptrdiff_t diff = a - b;

cout <<"a=" << a <<" b=" << b <<" diff=" << diff;

[请注意,可能是这里的大多数常规人,学徒,上面的a-b表达式会调用未定义的行为,因为减去一个分配的地址和另一个分配的地址是未定义的行为。这是为了应对没有线性内存地址的机器,例如分段存储器或"根据其类型将不同类型的数据存储在位置中"。上面的内容肯定可以在任何不使用分段内存模型且在堆中使用多个数据段的基于x86的操作系统上运行-这意味着它肯定可以在32位和64位模式下用于Windows和Linux。

您可能需要使用各种类型来运行它-只需记住diff位于"类型的数量"中,因此,如果将其设为int *a, *b,则它将以"四个字节为单位"。您可以使reinterpret_cast(a) - reinterpret_cast(b);

[diff可能为负,并且如果您在循环中运行此命令(不删除ab),则可能会发现突然的跳转,耗尽了很大一部分内存,而运行时库又分配了另一个大块]


Visual C ++在已分配缓冲区的边界附近嵌入控制信息(链接/大小和可能的某些校验和)。这也有助于捕获内存分配和释放期间的某些缓冲区溢出。

最重要的是,您应该记住,malloc()需要返回适合所有基本类型(charintlong longdoublevoid*void(*)())的指针。通常具有最大类型的大小,因此它可能是8个甚至16个字节。如果分配一个字节,则7到15个字节只能丢失以对齐。我不确定operator new是否具有相同的行为,但情况确实如此。

这应该给您一个想法。只能从文档(如果有)或测试中确定确切的内存浪费。语言标准没有任何定义。


是。所有实用的动态内存分配器都具有最小的粒度1。例如,如果粒度为16个字节,而您仅请求1个字节,则整个16个字节仍被分配。如果要求17个字节,则会分配一个大小为32个字节的块,依此类推...

还有一个(相关的)对齐问题。2

相当多的分配器似乎是大小图和空闲列表的组合-他们将潜在的分配大小划分为"存储桶",并为每个分配器保留一个单独的空闲列表。看看Doug Lea的malloc。还有许多其他分配技巧需要权衡取舍,但这超出了这里的范围...

1通常为8或16个字节。如果分配器使用空闲列表,则它必须在每个空闲插槽内编码两个指针,因此空闲插槽不能小于8字节(32位)或16字节(16位)。例如,如果分配器尝试拆分8个字节的插槽以满足4个字节的请求,则其余4个字节将没有足够的空间来编码空闲列表指针。

2例如,如果您平台上的long long是8字节,那么即使分配器的内部数据结构可以处理小于该值的块,实际上分配较小的块也可能会将下一个8字节分配推到未对齐的内存地址。