【问题标题】:Is there a memory overhead associated with heap memory allocations (eg markers in the heap)?是否存在与堆内存分配相关的内存开销(例如堆中的标记)?
【发布时间】:2013-03-30 15:46:15
【问题描述】:

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

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

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

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

【问题讨论】:

  • 通常,当您分配数组时,一些字节以分配的大小存储在开头以供将来删除。对于大型分配,开销几乎为零。如果你一次分配 4 个字节,它可以很好地使内存翻倍。

标签: c++ windows visual-studio memory-management heap-memory


【解决方案1】:

是的,当然。

分配的每个内存块都会有一个恒定的“标题”开销,以及一个小的可变部分(通常在末尾)。究竟有多少取决于使用的确切 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]。

您可能希望以不同的类型运行它 - 请记住,差异在“类型的编号,因此如果您将其设为 int *a, *b 将采用“四个字节单位”。您可以制作一个 @ 987654324@

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

【讨论】:

  • 我已经为此添加了评论。如果您有更好的方法来确定两个独立分配之间的距离,请随时提出建议。
  • 不不,我不是在暗示,只是指出一些事情。为了证明这样的东西,有时你会求助于 UB,那里没问题 :)
  • 啊,在您为原始评论提供答案时编辑了我的评论。对不起任何其他读者... ;)
  • 谢谢@Mats。我没有认为堆分配器是如此可预测,以至于在大多数情况下,连续(相等大小)分配之间的差异给出了内存开销的可靠指示。我可以试试看。
  • 只要您不在多个线程中运行它,或者在调用 new 之间调用一些在堆上分配的函数,应该没问题。当然,这适用于使用一大块内存来提供不同分配的堆......可能会有不这样做的分配器。此外,如果在您访问此代码时,您已经对底层堆进行了一系列 alloc/free 调用,它可能会重用来自这些调用的内存块来释放它们——这当然可能超出订购。
【解决方案2】:

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

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

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

【讨论】:

    【解决方案3】:

    是的。所有实用的动态内存分配器都具有最小的粒度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 个字节,那么即使分配器的内部数据结构可以处理小于该值的块,实际分配较小的块可能将下一个 8 字节分配推送到未对齐的内存地址。

    【讨论】:

    • 感谢@Branko 提供的信息和 malloc 链接,这似乎是对堆管理设计的非常好的基本介绍。
    猜你喜欢
    • 2011-05-28
    • 1970-01-01
    • 2014-10-03
    • 2019-12-10
    • 2020-09-01
    • 2023-03-03
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多