9

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

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

我对现代实现了解不多,但我想知道每次分配是否有标记写入,或者是否维护某种表(如文件分配表)。

在相关点上(因为我主要考虑像“map”这样的标准库功能),Microsoft 标准库实现是否曾经使用自己的分配器(用于树节点之类的东西)来优化使用?

4

3 回答 3

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;

[请注意,可能是这里的大多数正则,上面的 ab 表达式调用未定义的行为,因为减去一个已分配的地址和另一个的地址,是未定义的行为。这是为了应对没有线性内存地址的机器,例如分段内存或“不同类型的数据存储在基于它们的类型的位置”。以上内容绝对适用于任何不使用分段内存模型且堆中有多个数据段的基于 x86 的操作系统 - 这意味着它肯定适用于 32 位和 64 位模式的 Windows 和 Linux]。

您可能希望以不同的类型运行它 - 请记住,差异在“类型的数量”中,因此如果您将其int *a, *b设置为“四个字节单位”。您可以制作一个reinterpret_cast<char*>(a) - reinterpret_cast<char *>(b);

[diff 可能是负数,如果你在循环中运行它(不删除aand b),你可能会发现一大块内存耗尽的突然跳转,并且运行时库分配了另一个大块]

于 2013-04-08T14:14:19.220 回答
4

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

最重要的是,您应该记住malloc()需要返回针对所有基本类型(char, int, long long, double, void*, void(*)())适当对齐的指针,并且对齐通常是最大类型的大小,因此它可能是 8 甚至 16 个字节。如果分配单个字节,则仅对齐可能会丢失 7 到 15 个字节。我不确定是否operator new有相同的行为,但很可能是这种情况。

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

于 2013-04-08T14:16:01.093 回答
2

是的。所有实际的动态内存分配器都具有最小粒度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 字节分配推到未对齐的内存地址。

于 2013-04-08T15:05:55.697 回答