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