特别考虑使用最近的Visual Studio C ++编译器的Windows上的C ++,我想知道堆的实现:
假设我使用的是发行版编译器,并且我不关心内存碎片/打包问题,那么与在堆上分配内存相关的内存开销是否存在? 如果是这样,大概每个分配有多少个字节?
64-bit代码中的代码是否大于32-bit?
我对现代堆的实现并不太了解,但是想知道是否在每次分配时都将标记写入堆中,或者是否维护了某种表(例如文件分配表)。
在一个相关点上(因为我主要考虑的是诸如" map"之类的标准库功能),Microsoft标准库实现是否曾经使用自己的分配器(用于诸如树节点的分配器)来优化堆使用?
-
通常,当您分配数组时,某些字节会以分配的大小存储在开头,以供将来删除。 对于大分配,开销几乎没有。 如果一次分配4个字节,则可以使内存增加一倍。
是的,一点没错。
分配的每个内存块都将有恒定的"头"开销,以及较小的可变部分(通常在末尾)。究竟有多少取决于所使用的确切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可能为负,并且如果您在循环中运行此命令(不删除
a和
b),则可能会发现突然的跳转,耗尽了很大一部分内存,而运行时库又分配了另一个大块]
-
从技术上讲,a-b是UB。
-
香港专业教育学院添加了对此的评论。如果您有更好的方法来确定两个独立分配之间的距离的建议,请随时提出建议。
-
不,不,我并不是要暗示,只是指出一些东西。为了证明这样的东西,有时您求助于UB,那里没有问题:)
-
嗯,您在提供原始评论的答案时编辑了我的评论。对不起其他读者...;)
-
谢谢@马特。我还没有考虑过堆分配器的可预测性,因此在大多数情况下,连续(相等大小)分配之间的差异可以可靠地表明内存开销。我可以尝试一下。
-
只要您不在多个线程中运行它,或者在调用new之间调用某个在堆上分配的函数,就可以了。当然,这适用于使用一大块内存提供不同分配的堆...可能有不这样做的分配器。另外,如果在您获得这段代码时,您已经对基础堆进行了许多alloc / free调用,则它可能会重用来自那些调用的内存块以释放它们-当然,这些内存块可能会乱序。
-
"如果您有更好的方法来确定两个独立分配之间的距离的建议,请随时提出建议",我知道这篇文章已经很老了,但是您可以将指针投射到std::uintptr_t(这是其中一种情况,我更喜欢C风格的演员)
-
@MikeMB:嗯,是的,但原因并不在于UB不是"它将爆炸并导致WW III启动",而是"实际结果是不确定的"-这是因为,例如,分段内存系统不会有一种定义明确的方法来确定不同段中两个位置之间的距离(以字节为单位)(x86保护模式段在该段和该段的基址之间没有直接关系,基址在描述符表(用户模式代码不可用)。
-
(续)。因此,在这样的系统中,由于segA和segB相隔8,并存储在uintptr_t的上半部分,而uintptr_t ai = (uintptr_t)a; uintptr_t bi = (uintptr_t)b;会简单地显示出32 GB的差异,而在uintptr_t的下半部分则存储零。 x5>。例如,在具有"本地"和"全局"存储段的基于GPU的系统中可以发现相同的效果。然后,即使可以将它们变成整数,也无法有意义地利用具有不同地址空间(段)的指针之间的差异。 [并且使用reinterpret_cast(a)是我的选择]。
-
@MatsPetersson:也许我对您有误解,但是:如果您在具有分段内存的平台上,那么无论如何您都不会得到可靠的答案(无论您将指针投射到char*还是uintptr_t)。但是,当您使用内存空间较小的系统时,带有char*的版本仍然是未定义的行为,而uintptr_t会为您提供明确的答案。
-
现在,传统上您可以说。构造X被指定为UB,因为在platfomr Z上您没有实现有意义的/一致的行为,但是在所有其他平台上都应该没有问题。但是,编译器在基于UB的优化中变得越来越积极。例如。如果编译器可以证明一段代码取消了对nullptr的引用,它将仅将其标记为不可访问。谁知道将来的编译器会做什么,何时他们可以证明您正在比较不相关的指针(当直接从malloc获取它们时,这很容易)。
-
在这种情况下,这当然很明显。我不确定如果您从UB减中打印出差异,编译器会回答什么,但我希望它显而易见。除非您启用至少某种程度的优化,否则可能不会发生...
-
"给脚步者的注意,这可能是这里的大多数常客",这让我有点笑了;)
Visual C ++在已分配缓冲区的边界附近嵌入控制信息(链接/大小和可能的某些校验和)。这也有助于捕获内存分配和释放期间的某些缓冲区溢出。
最重要的是,您应该记住,malloc()需要返回适合所有基本类型(char,int,long long,double,void*,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字节分配推到未对齐的内存地址。
-
感谢信息和malloc链接@Branko,这似乎是堆管理设计的很好的基础介绍。