访问堆中的数据是否比从堆栈中访问数据快?

新手上路,请多包涵

我知道这听起来像是一个普遍的问题,我已经看到了许多类似的问题(在这里和网上),但没有一个真的像我的困境。

说我有这个代码:

 void GetSomeData(char* buffer)
{
    // put some data in buffer
}

int main()
{
     char buffer[1024];
     while(1)
     {
          GetSomeData(buffer);
          // do something with the data
     }
     return 0;
}

如果我在全局范围内声明 buffer[1024],我会获得任何性能吗?

我通过 time 命令在 unix 上运行了一些测试,执行时间之间几乎没有差异。

但我不是很相信…

从理论上讲,这种变化应该有所作为吗?

原文由 conectionist 发布,翻译遵循 CC BY-SA 4.0 许可协议

阅读 551
2 个回答

访问堆中的数据是否比从堆栈中访问数据快?

不是天生的……在我曾经工作过的每个架构上,所有进程“内存”都可以预期以相同的速度运行,这取决于 CPU 缓存/RAM/交换文件的哪个级别保存当前数据,以及该内存上的操作可能触发的任何硬件级同步延迟,以使其对其他进程可见,合并其他进程/CPU(核心)的更改等。

操作系统(负责页面错误/交换)和硬件(CPU)捕获对尚未访问或换出页面的访问,甚至不会跟踪哪些页面是“全局”与“堆栈”与“堆”…内存页是内存页。

虽然操作系统和硬件不知道放置内存的全局、堆栈和堆的使用情况,并且所有这些都由具有相同性能特征的相同类型的内存支持,但还有其他一些微妙的考虑(在此列表后详细描述) :

  • 分配- 程序花费“分配”和“取消分配”内存的时间,包括随着堆使用量的增长,偶尔 sbrk (或类似的)虚拟地址分配
  • 访问- 程序用于访问全局、堆栈和堆的 CPU 指令的差异,以及在使用基于堆的数据时通过运行时指针进行的额外 间接 访问,
  • 布局- 某些数据结构(“容器”/“集合”)对缓存更友好(因此更快),而某些数据结构的通用实现需要堆分配并且可能对缓存不太友好。

分配和解除分配

对于 全局数据(包括 C++ 命名空间数据成员),虚拟地址通常会在 编译时 计算和硬编码(可能是绝对值,或者作为段寄存器的偏移量;有时它可能需要调整,因为进程由操作系统)。

对于基于 堆栈 的数据,堆栈指针寄存器相对地址也可以在 编译时 计算和硬编码。然后堆栈指针寄存器可以通过函数参数的总大小、局部变量、返回地址和保存的 CPU 寄存器在函数进入和返回时进行调整(即在运行时)。添加更多基于堆栈的变量只会改变用于调整堆栈指针寄存器的总大小,而不是产生越来越不利的影响。

以上两者实际上都没有运行时分配/释放开销,而基于堆的开销是非常真实的,并且对于某些应用程序可能很重要……

对于基于 的数据, 运行时 堆分配库必须查询并更新其内部数据结构,以跟踪它管理的块(即堆内存池)的哪些部分与库提供给应用程序,直到应用程序释放或删除内存。如果堆内存的虚拟地址空间不足,可能需要调用 sbrk 类的 OS 函数来请求更多内存(Linux 也可以调用 mmap 为大内存创建后备内存请求,然后在 free / delete 上取消映射该内存。

使用权

因为可以在编译时为全局和基于堆栈的数据计算绝对虚拟地址或段或堆栈指针寄存器相对地址,所以运行时访问非常快。

对于堆托管数据,程序必须通过运行时确定的指针访问数据,该指针保存堆上的虚拟内存地址,有时在运行时使用指向特定数据成员的指针的偏移量。在某些架构上,这可能需要更长的时间。

对于堆访问,指针和堆内存都必须在寄存器中才能访问数据(因此对 CPU 缓存有更多需求,并且在规模上 - 更多缓存未命中/故障开销)。

注意:这些成本通常是微不足道的——甚至不值得一看或重新考虑,除非你正在编写延迟或吞吐量非常重要的东西。

布局

如果源代码的连续行列出全局变量,它们将排列在相邻的内存位置(尽管出于对齐目的可能有填充)。对于同一函数中列出的基于堆栈的变量也是如此。这很棒:如果您有 X 字节的数据,您可能会发现 - 对于 N 字节缓存行 - 它们被很好地打包到内存中,可以使用 X/N 或 X/N + 1 缓存行访问。您的程序很可能同时需要附近的其他堆栈内容 - 函数参数、返回地址等,因此缓存非常有效。

当您使用基于堆的内存时,对堆分配库的连续调用可以轻松地在不同的缓存行中返回指向内存的指针,特别是如果分配大小相差一个公平位(例如,三个字节分配后跟一个 13 字节分配)或者如果有已经有很多分配和释放(导致“碎片化”)。这意味着当您访问一堆小堆分配的内存时,最坏的情况是您可能需要在尽可能多的缓存行中出错(除了需要加载包含指向堆的指针的内存)。堆分配的内存不会与堆栈分配的数据共享缓存行 - 那里没有协同作用。

此外,C++ 标准库不提供更复杂的数据结构——如链表、平衡二叉树或哈希表——设计用于基于堆栈的内存。因此,当使用堆栈时,程序员倾向于使用在内存中连续的数组来做他们可以做的事情,即使这意味着有点暴力搜索。与元素分布在更多缓存行中的基于堆的数据容器相比,缓存效率可能会使其总体上更好。当然,堆栈的使用不会扩展到大量元素,并且 - 至少没有使用堆的备份选项 - 如果要处理的数据多于预期,创建的程序会停止工作。

讨论您的示例程序

在您的示例中,您将全局变量与函数局部(堆栈/自动)变量进行对比……不涉及堆。堆内存来自 newmalloc / realloc 。对于堆内存,值得注意的性能问题是应用程序本身正在跟踪哪些地址正在使用多少内存 - 所有需要一些时间来更新的记录作为指向内存的指针由 new / malloc / realloc , and some more time to update as the pointers are delete d or free d.

对于全局变量,内存分配可以在编译时有效地完成,而对于基于堆栈的变量,通常有一个堆栈指针,每次都会由编译时计算的局部变量(和一些内务数据)大小的总和递增一个函数被调用。因此,当调用 main() 时,可能需要一些时间来修改堆栈指针,但如果没有 buffer 并且如果有,所以运行时性能完全没有区别。

笔记

我省略了上面一些无聊且基本上不相关的细节。例如,一些 CPU 在调用另一个函数时使用寄存器的“窗口”来保存一个函数的状态;某些函数状态将保存在寄存器中而不是堆栈中;一些函数参数将通过寄存器而不是堆栈传递;并非所有操作系统都使用虚拟寻址;一些非 PC 级硬件可能具有更复杂的内存架构,具有不同的含义….

原文由 Tony Delroy 发布,翻译遵循 CC BY-SA 4.0 许可协议

有关此主题的博客文章 stack-allocation-vs-heap-allocation-performance-benchmark 显示了分配策略基准。测试是用 C 语言编写的,并在纯分配尝试和内存初始化分配之间进行比较。在不同的总数据大小下,执行循环次数并测量时间。每个分配由 10 个不同大小的 alloc/init/free 块组成(总大小显示在图表中)。

测试在 Intel® Core™ i7-6600U CPU、Linux 64 位、4.15.0-50-generic、Spectre 和 Meltdown 补丁禁用。

没有初始化: 没有数据初始化的内存分配

使用初始化: 使用数据初始化的内存分配

在结果中,我们看到没有数据初始化的纯分配存在显着差异。堆栈比堆快,但请注意循环计数非常高。

在处理分配的数据时,堆栈和堆性能之间的差距似乎缩小了。在 1M malloc/init/free(或堆栈分配)循环中,每个循环尝试 10 次分配,就总时间而言,堆栈仅比堆提前 8%。

原文由 Madars Vi 发布,翻译遵循 CC BY-SA 4.0 许可协议

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题