使用最新的Visual Studio C ++编译器在Windows上特别考虑C ++,我想知道堆实现:
假设我正在使用发布编译器,并且我不关心内存碎片/打包问题,是否存在与在堆上分配内存相关的内存开销?如果是这样,大概每个分配的字节数可能是多少?
它会64-bit
代码比32-bit
更大吗?
我对现代堆实现并不是很了解,但我想知道每次分配是否有写入堆的标记,或者是否某种维护表(如文件分配表)。
在一个相关点上(因为我主要考虑的是像'map'这样的标准库功能),Microsoft标准库实现是否曾使用自己的分配器(对于像树节点这样的东西)来优化堆用法?
答案 0 :(得分:7)
是的,绝对的。
分配的每个内存块将具有“标头”的常量开销,以及小的可变部分(通常在末尾)。究竟有多少取决于使用的确切C运行时库。在过去,我通过实验发现每个分配大约32-64字节。可变部分是为了应对对齐 - 每个内存块将与一些不错的甚至2 ^ n基址对齐 - 通常为8或16个字节。
我不熟悉std::map
或类似作品的内部设计如何运作,但我非常怀疑它们在那里有特殊的优化。
您可以通过以下方式轻松测试开销:
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<char*>(a) - reinterpret_cast<char *>(b);
[diff可能是负数,如果你在循环中运行它(不删除a
和b
),你可能会发现一大段内存耗尽的突然跳转,以及运行时库分配了另一个大块]
答案 1 :(得分:4)
Visual C ++在分配的缓冲区边界附近嵌入控制信息(链接/大小和可能的某些校验和)。这也有助于在内存分配和释放期间捕获一些缓冲区溢出。
最重要的是你应该记住malloc()
需要返回适合所有基本类型的指针(char
,int
,long long
,double
,void*
,void(*)()
)并且该对齐通常具有最大类型的大小,因此它可以是8或甚至16个字节。如果分配单个字节,则只能丢失7到15个字节才能进行对齐。我不确定operator new
是否有相同的行为,但情况可能就是这样。
这应该会给你一个想法。精确的内存浪费只能通过文档(如果有)或测试来确定。语言标准没有以任何术语定义它。
答案 2 :(得分:2)
是。所有实用的动态内存分配器都具有最小的粒度 1 。例如,如果粒度为16个字节而您只请求1个字节,则仍会分配整个16个字节。如果要求17个字节,则分配大小为32字节的块等...
还有一个(相关的)对齐问题。 2
相当多的分配器似乎是大小映射和自由列表的组合 - 它们将潜在的分配大小分成“桶”,并为每个分配器保留单独的空闲列表。看看Doug Lea's malloc。还有许多其他分配技术有各种权衡,但超出了这里的范围...
1 通常为8或16个字节。如果分配器使用空闲列表,则它必须在每个空闲时隙内编码两个指针,因此空闲时隙不能小于8个字节(在32位上)或16个字节(在16位上)。例如,如果分配器尝试拆分8字节的插槽以满足4字节的请求,则剩余的4个字节将没有足够的空间来编码空闲列表指针。
2 例如,如果平台上的long long
是8个字节,那么即使分配器的内部数据结构可以处理小于此的块,实际上也会分配较小的块。 block可能会将下一个8字节分配推送到未对齐的内存地址。