【问题标题】:Is accessing data in the heap faster than from the stack?访问堆中的数据是否比从堆栈中访问数据快?
【发布时间】:2014-07-26 06:39:56
【问题描述】:

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

假设我有这个代码:

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 上运行了一些测试,执行时间之间几乎没有差异。

但我不是很相信...

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

【问题讨论】:

  • 这与您访问内存的速度无关(除非我们谈论 NUMA 之类的东西),而是通过多少次间接访问它。
  • 据我所知,从堆访问稍微慢一些。但是,您不应该考虑这一点。默认情况下,您应该在堆栈中分配所有内容,除非您需要堆上的数据。
  • 从堆访问是间接原因稍慢,请查看@PlasmaHH 评论。栈内存和堆内存没有区别,它们都在 RAM 中。
  • “理论上” - 该标准足够抽象,无法对此进行规范。那么问题是什么?
  • 这应该被标记为与 allocation 性能有关的问题的重复,而它是关于 access 性能的。

标签: c++ c performance heap-memory stack-memory


【解决方案1】:

访问堆中的数据比访问堆栈中的数据快吗?

并非天生...在我曾经工作过的每个架构上,所有进程“内存”都可以预期以相同的速度运行,这取决于 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++ 标准库不提供更复杂的数据结构 - 如链表、平衡二叉树或哈希表 - 专为在基于堆栈的内存中使用而设计。因此,在使用堆栈时,程序员倾向于使用在内存中连续的数组来做他们可以做的事情,即使这意味着有点暴力搜索。与元素分布在更多缓存行中的基于堆的数据容器相比,缓存效率可能会使其总体上更好。当然,堆栈的使用不会扩展到大量元素,并且 - 至少没有使用堆的备份选项 - 如果处理的数据多于预期,创建的程序会停止工作。

讨论你的示例程序

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

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

注意

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

【讨论】:

  • 回复你的第一句话:我开始写同样的东西,但正如你在下文中指出的那样,它不是正确的;事实(在今天的大多数处理器上)是,速度本身并不取决于内存所在的位置,而是取决于之前访问过的内容。
  • @JamesKanze “这不是真的” - 好吧,取决于观点 - 缓存未命中确实比缓存命中慢(在任何缓存级别),并且相同的步进性能配置文件适用于全局+静态/堆栈/堆/线程特异性/共享/等。内存可能被应用程序使用...这是我的意图,尽管我同意它可以更好地措辞和会有一个裂缝。
  • @Tony D:你能澄清我的困惑吗?因此,通过访问(写入/加载),堆栈与堆的速度大致相同,但它在分配方面应该更快,因为它已经在编译器时完成,不会增加运行开销?谢谢
  • @dragonxlwang:差不多大小,是的。干杯。
  • 这是一个出色而彻底的答案。太感谢了。它确实消除了我对为什么堆栈和堆尽管都分配在 RAM 中但具有不同的性能特征的许多困惑。特别是,可以在编译时计算出堆栈指针这一事实是一个巨大的洞察力!
【解决方案2】:

引用Jeff Hill's answer

堆栈更快因为访问模式使得从中分配和释放内存变得微不足道(指针/整数只是递增或递减),而堆涉及更复杂的簿记分配或免费。此外,堆栈中的每个字节往往被非常频繁地重用,这意味着它往往被映射到处理器的缓存,从而使其非常快。堆的另一个性能损失是堆,主要是全局资源,通常必须是多线程安全的,即每个分配和释放需要 - 通常 - 与程序中的“所有”其他堆访问同步。

【讨论】:

  • "访问堆中的数据比从栈中访问数据快吗?"问题是,你的强调实际上是错误的,如果你有相同的数据和相同的访问模式,那么理论上堆应该和堆栈一样快。如果您的数据是一个数组,只要数据是连续的,访问 应该花费相同的时间。如果您在 ram 中随处可见几小位数据,则​​堆栈的时间会更快。
【解决方案3】:

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

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

没有初始化:

使用初始化:

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

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

【讨论】:

    【解决方案4】:

    您的问题并没有真正的答案;这取决于什么 否则你正在做。一般来说,大多数机器使用 在整个过程中使用相同的“内存”结构,所以不管 变量所在的位置(堆、堆栈或全局内存), 访问时间将相同。另一方面,最现代的 机器有一个分层的内存结构,有一个内存 流水线、多级缓存、主存和虚拟 记忆。取决于之前发生的事情 处理器,实际访问可能是这些中的任何一个 (无论是堆、堆栈还是全局),并且 这里的访问时间变化很大,如果 内存在管道中的正确位置,对某事 如果系统必须进入虚拟内存,大约需要 10 毫秒 在磁盘上。

    在所有情况下,关键是位置。如果访问是“附近” 以前的访问,您大大提高了找到它的机会 在更快的位置之一:例如缓存。在这个 考虑到,将较小的对象放在堆栈上可能会更快, 因为当你访问一个函数的参数时,你 访问堆栈内存(使用 Intel 32 位处理器,在 最少——使用更好设计的处理器,争论更多 可能在寄存器中)。但这可能不会是 涉及数组时的问题。

    【讨论】:

    • 所以基本上为了准确比较堆栈速度和堆速度,我们应该禁用 CPU 缓存?
    【解决方案5】:

    在堆栈上分配缓冲区时,优化范围不是访问内存的成本,而是消除堆上通常非常昂贵的动态内存分配(堆栈缓冲区分配可以被认为是瞬时的,因为堆栈作为一个整体分配在线程启动)。

    【讨论】:

      【解决方案6】:

      对于它的价值,下面代码中的循环——它只是读取和写入一个大数组中的每个元素——当数组在堆栈上时,在我的机器上运行速度始终快 5 倍,而不是在堆上时( GCC、Windows 10、-O3 标志),甚至在重新启动后(当堆碎片最小化时):

      const int size = 100100100;
      int vals[size]; // STACK
      // int *vals = new int[size]; // HEAP
      startTimer();
      for (int i = 1; i < size; ++i) {
          vals[i] = vals[i - 1];
      }
      stopTimer();
      std::cout << vals[size - 1];
      // delete[] vals; // HEAP
      

      当然,我首先必须将堆栈大小增加到 400 MB。请注意,需要在末尾打印最后一个元素,以防止编译器优化所有内容。

      【讨论】:

      • 如何增加堆栈大小?
      • @PaimanRoointan linux下可以使用ulimit -s
      【解决方案7】:

      给定在堆上声明的变量和变量数组更慢只是一个事实。这样想;

      全局创建的变量只分配一次,程序关闭后就释放。对于堆对象,您的变量必须在每次运行函数时就地分配,并在函数结束时释放..

      曾经尝试过在函数中分配对象指针吗?在函数退出之前更好地释放/删除它,否则你将有一个内存泄漏,因为你没有在解构函数内释放/删除它的类对象中执行此操作。

      在访问数组时,它们的工作方式相同,首先由 sizeof(DataType) * 个元素分配内存块。稍后可以通过 ->

      访问
      1 2 3 4 5 6 
      ^ entry point [0]
            ^ entry point [0]+3
      

      【讨论】:

      • 堆和堆栈分配是完全不同的野兽。堆栈分配实际上是免费的,所以不管你必须做多少次。
      • 被否决了 3 次,但没有人解释这个答案有什么问题。所以我要 +1。
      猜你喜欢
      • 2015-01-09
      • 2011-03-28
      • 2021-10-13
      • 1970-01-01
      • 2019-07-18
      • 1970-01-01
      • 1970-01-01
      • 2016-08-28
      • 2014-01-09
      相关资源
      最近更新 更多