访问堆中的数据比访问堆栈中的数据快吗?
并非天生...在我曾经工作过的每个架构上,所有进程“内存”都可以预期以相同的速度运行,这取决于 CPU 缓存/RAM/交换文件的级别当前数据,以及对该内存的操作可能触发的任何硬件级同步延迟,以使其对其他进程可见,合并其他进程/CPU(核心)的更改等。
操作系统(负责页面错误/交换)和硬件 (CPU) 捕获对尚未访问或已换出页面的访问,甚至不会跟踪哪些页面是“全局”与“ stack" vs "heap"...内存页就是内存页。
虽然内存所在的全局 vs 堆栈 vs 堆使用情况对于操作系统和硬件来说是未知的,并且都由具有相同性能特征的相同类型的内存支持,但还有其他一些微妙的考虑因素(在后面详细描述此列表):
-
分配 - 程序花费“分配”和“解除分配”内存的时间,包括随着堆使用量的增长而偶尔分配
sbrk(或类似的)虚拟地址
-
访问 - 程序用于访问全局、堆栈和堆的 CPU 指令的差异,以及额外的间接在使用基于堆的数据时通过运行时指针,
-
布局 - 某些数据结构(“容器”/“集合”)对缓存更友好(因此速度更快),而某些数据结构的通用实现需要堆分配并且可能对缓存不太友好。
分配和解除分配
对于全局数据(包括 C++ 命名空间数据成员),虚拟地址通常会在编译时计算和硬编码(可能是绝对值,或作为偏移量)来自段寄存器;有时它可能需要在操作系统加载进程时进行调整)。
对于基于堆栈的数据,堆栈指针寄存器相对地址也可以在编译时计算和硬编码。然后堆栈指针寄存器可以通过函数参数、局部变量、返回地址和保存的 CPU 寄存器的总大小在函数进入和返回时进行调整(即在运行时)。添加更多基于堆栈的变量只会改变用于调整堆栈指针寄存器的总大小,而不是产生越来越不利的影响。
上述两种方法实际上都没有运行时分配/释放开销,而基于堆的开销是非常真实的,并且对于某些应用程序可能很重要......
对于基于 heap 的数据,runtime 堆分配库必须查阅和更新其内部数据结构,以跟踪块的哪些部分(即池) ) 它管理的堆内存与库提供给应用程序的特定指针相关联,直到应用程序释放或删除内存。如果堆内存的虚拟地址空间不足,它可能需要调用像sbrk 这样的OS 函数来请求更多内存(Linux 也可以调用mmap 为大内存请求创建后备内存,然后在@ 上取消映射该内存987654324@/delete)。
访问
由于可以在编译时为全局和基于堆栈的数据计算绝对虚拟地址或段或堆栈指针寄存器相对地址,因此运行时访问非常快。
对于堆托管数据,程序必须通过运行时确定的指针来访问数据,该指针保存堆上的虚拟内存地址,有时会在运行时使用指向特定数据成员的指针的偏移量。这在某些架构上可能需要更长的时间。
对于堆访问,指针和堆内存都必须在寄存器中才能访问数据(因此对 CPU 缓存有更多需求,并且在规模上 - 更多缓存未命中/故障开销)。
注意:这些成本通常是微不足道的 - 甚至不值得一看或重新考虑,除非您正在编写延迟或吞吐量非常重要的东西。
布局
如果源代码的连续行列出了全局变量,它们将排列在相邻的内存位置(尽管出于对齐目的可能有填充)。对于同一函数中列出的基于堆栈的变量也是如此。这很棒:如果您有 X 字节的数据,您可能会发现 - 对于 N 字节缓存行 - 它们被很好地打包到内存中,可以使用 X/N 或 X/N + 1 缓存行访问。您的程序很可能同时需要附近的其他堆栈内容 - 函数参数、返回地址等,因此缓存非常有效。
当您使用基于堆的内存时,对堆分配库的连续调用可以轻松地返回指向不同缓存行中内存的指针,尤其是在分配大小相差相当大的情况下(例如,三字节分配后跟 13 字节分配)或者如果已经有很多分配和释放(导致“碎片”)。这意味着当您访问一堆小堆分配的内存时,最坏的情况是您可能需要在尽可能多的缓存行中出错(除了需要加载包含指向堆的指针的内存)。堆分配的内存不会与堆栈分配的数据共享缓存行 - 没有协同作用。
此外,C++ 标准库不提供更复杂的数据结构 - 如链表、平衡二叉树或哈希表 - 专为在基于堆栈的内存中使用而设计。因此,在使用堆栈时,程序员倾向于使用在内存中连续的数组来做他们可以做的事情,即使这意味着有点暴力搜索。与元素分布在更多缓存行中的基于堆的数据容器相比,缓存效率可能会使其总体上更好。当然,堆栈的使用不会扩展到大量元素,并且 - 至少没有使用堆的备份选项 - 如果处理的数据多于预期,创建的程序会停止工作。
讨论你的示例程序
在您的示例中,您将全局变量与函数局部(堆栈/自动)变量进行对比......不涉及堆。堆内存来自new 或malloc/realloc。对于堆内存,值得注意的性能问题是应用程序本身正在跟踪哪些地址正在使用多少内存 - 所有需要一些时间来更新的记录作为指向内存的指针由new/ malloc/realloc,还有一些时间来更新,因为指针是 deleted 或 freed。
对于全局变量,内存分配可以在编译时有效地完成,而对于基于堆栈的变量,通常有一个堆栈指针,该指针由编译时计算的局部变量(以及一些内务数据)的大小之和递增) 每次调用函数时。因此,当调用main() 时,可能需要一些时间来修改堆栈指针,但如果没有buffer,它可能只是被修改了不同的数量而不是不修改,如果有则修改,所以没有区别运行时性能。
注意
我省略了上面一些无聊且基本上不相关的细节。例如,一些 CPU 在调用另一个函数时使用寄存器的“窗口”来保存一个函数的状态;某些函数状态将保存在寄存器中而不是堆栈中;一些函数参数将通过寄存器而不是堆栈传递;并非所有操作系统都使用虚拟寻址;一些非 PC 级硬件可能具有更复杂的内存架构,具有不同的含义....