当前位置:  开发笔记 > 编程语言 > 正文

哪个更快:堆栈分配或堆分配

如何解决《哪个更快:堆栈分配或堆分配》经验,为你挑选了8个好方法。

这个问题可能听起来相当简单,但这是我与另一位与我合作的开发人员的辩论.

我正在小心处理堆栈分配的东西,而不是堆分配它们.他正在跟我说话,看着我的肩膀并评论说这没有必要,因为他们的表现是明智的.

我一直认为堆栈的增长是恒定的时间,并且堆分配的性能取决于堆的当前复杂性(用于找到合适大小的孔)和解除分配(折叠孔以减少碎片,如如果我没有弄错的话,许多标准库实现在删除期间需要时间来完成此操作.

这让我觉得可能非常依赖于编译器.特别是对于这个项目,我使用Metrowerks编译器来实现PPC架构.对这种组合的洞察力将是最有帮助的,但总的来说,对于GCC和MSVC++,情况如何?堆分配不如堆栈分配高吗?没有区别吗?或者差异是如此微小,它变得毫无意义的微优化.



1> Torbjörn Gyl..:

堆栈分配要快得多,因为它真正做的就是移动堆栈指针.使用内存池,您可以从堆分配中获得可比较的性能,但这会带来轻微的复杂性和自身的麻烦.

此外,堆栈与堆不仅是性能考虑因素; 它还告诉你很多关于对象的预期寿命.


更重要的是,堆栈总是很热,你得到的内存比任何远堆分配的内存更容易在缓存中
在一些(主要是嵌入的,我所知道的)架构上,堆栈可以存储在快速片上存储器(例如SRAM)中.这可以产生巨大的差异!
因为堆栈实际上是一个堆栈.你不能释放堆栈使用的一块内存,除非它在它之上.没有管理,你推动或弹出它.另一方面,堆内存被管理:它向内核请求内存块,可能会拆分它们,合并它们,重用它们并释放它们.堆栈实际上是用于快速和短期分配.
@Pacerier因为Stack比Heap小很多.如果要分配大数组,最好在堆上分配它们.如果你试图在Stack上分配一个大数组,它会给你一个Stack Overflow.以C++为例:int t [100000000]; 试试例如t [10000000] = 10; 然后cout << t [10000000]; 它应该给你一个堆栈溢出或只是不会工作,不会向你显示任何东西.但是如果你在堆上分配数组:int*t = new int [100000000]; 并且之后执行相同的操作,它将起作用,因为Heap具有这么大的数组所需的大小.
@Pacerier最明显的原因是,在退出分配的块时,堆栈上的对象超出了范围.
@Benoît你能解释为什么不把所有东西都存放在堆栈上?堆的重点是什么?

2> Dan Lenski..:

堆栈速度更快.在大多数情况下,例如在x86上,它实际上只在大多数体系结构上使用单个指令:

sub esp, 0x10

(这会将堆栈指针向下移动0x10字节,从而"分配"这些字节以供变量使用.)

当然,堆栈的大小非常非常有限,因为您将很快发现是否过度使用堆栈分配或尝试进行递归:-)

此外,没有理由优化不可验证地需要它的代码的性能,例如通过分析证明."过早优化"通常会导致更多问题,而不是它的价值.

我的经验法则:如果我知道我将在编译时需要一些数据,并且它的大小在几百字节之内,我会进行堆栈分配.否则我堆分配它.


一条指令,通常由堆栈中的所有对象共享.
请记住这里的"隐藏"成本,尤其是第一次扩展堆栈时.这样做可能会导致页面错误,上下文切换到内核需要做一些工作来分配内存(或者在最坏的情况下从交换中加载).
明确指出,特别是关于可验证地需要它的观点.我不断惊讶于人们对表现的担忧是如何错位的.
"释放"也非常简单,只需单个"离开"指令即可完成.
在某些情况下,您甚至可以分配0条指令。如果知道需要分配多少字节的信息,则编译器可以在分配其他堆栈变量的同时提前分配它们。在这种情况下,您根本不需支付任何费用!

3> Max Lybbert..:

老实说,编写一个比较性能的程序是微不足道的:

#include 
#include 

namespace {
    class empty { }; // even empty classes take up 1 byte of space, minimum
}

int main()
{
    std::clock_t start = std::clock();
    for (int i = 0; i < 100000; ++i)
        empty e;
    std::clock_t duration = std::clock() - start;
    std::cout << "stack allocation took " << duration << " clock ticks\n";
    start = std::clock();
    for (int i = 0; i < 100000; ++i) {
        empty* e = new empty;
        delete e;
    };
    duration = std::clock() - start;
    std::cout << "heap allocation took " << duration << " clock ticks\n";
}

据说愚蠢的一致性是小脑袋的大人物.显然优化编译器是许多程序员心目中的大地精.这个讨论过去是在答案的最底层,但人们显然无法阅读那么远,所以我将它移到这里以避免得到我已经回答过的问题.

优化编译器可能会注意到此代码不执行任何操作,并且可能会将其全部优化.做这样的事情是优化者的工作,与优化器作斗争是一个愚蠢的差事.

我建议在关闭优化的情况下编译此代码,因为没有好方法可以欺骗当前正在使用的每个优化器或将来使用的优化器.

任何打开优化器然后抱怨打架的人应该受到公众的嘲笑.

如果我关心纳秒精度,我就不会用std::clock().如果我想将结果作为博士论文发表,我会就此做出更大的贡献,我可能会比较GCC,Tendra/Ten15,LLVM,Watcom,Borland,Visual C++,Digital Mars,ICC和其他编译器.实际上,堆分配需要比堆栈分配长几百倍,而且我没有看到任何进一步调查问题的任何有用信息.

优化器的任务是摆脱我正在测试的代码.我没有看到任何理由告诉优化器运行然后尝试欺骗优化器而不是实际优化.但如果我看到这样做的价值,我会做以下一个或多个:

    添加数据成员empty,并在循环中访问该数据成员; 但如果我只读取数据成员,优化器可以进行常量折叠并删除循环; 如果我只写入数据成员,优化器可能会跳过除循环的最后一次迭代之外的所有迭代.此外,问题不是"堆栈分配和数据访问与堆分配和数据访问".

    声明e volatile,但volatile通常编译不正确(PDF).

    获取e循环内部的地址(并可能将其分配给extern在另一个文件中声明和定义的变量).但即使在这种情况下,编译器可能会注意到 - 至少在堆栈上 - e将始终在相同的内存地址分配,然后像上面的(1)那样进行常量折叠.我获得了循环的所有迭代,但实际上从未分配对象.

除了显而易见的,这个测试是有缺陷的,因为它测量分配和释放,原始问题没有询问释放.当然,在堆栈上分配的变量会在其作用域的末尾自动解除分配,因此不会调用delete(1)使数字倾斜(堆栈释放包含在堆栈分配的数字中,因此测量堆重新分配是公平的)和( 2)导致相当糟糕的内存泄漏,除非我们保留对新指针的引用并delete在我们进行时间测量后调用.

在我的机器上,在Windows上使用g ++ 3.4.4,对于任何小于100000分配的内容,我得到堆栈和堆分配的"0时钟滴答",即使这样,我得到堆栈分配的"0时钟滴答"和"15个时钟滴答" "用于堆分配.当我测量10,000,000个分配时,堆栈分配需要31个时钟周期,堆分配需要1562个时钟周期.


是的,优化编译器可能会忽略创建空对象.如果我理解正确,它甚至可能会忽略整个第一个循环.当我将迭代次数提高到10,000,000时,堆栈分配需要31个时钟周期,堆分配需要1562个时钟周期.我认为可以肯定地说,在没有告诉g ++来优化可执行文件的情况下,g ++并没有忽略构造函数.


自从我写这篇文章以来的几年里,Stack Overflow的偏好就是从优化版本中发布性能.总的来说,我认为这是正确的.但是,我仍然认为,当您实际上不希望优化代码时,要求编译器优化代码是愚蠢的.这让我觉得非常类似于为代客泊车支付额外费用,但拒绝交出钥匙.在这种特殊情况下,我不希望优化器运行.

使用略微修改的基准测试版本(以解决原始程序每次在循环中没有在堆栈上分配某些内容的有效点)并进行编译而不进行优化但链接到发布库(以解决我们不知道的有效点)我想包括链接到调试库导致的任何减速:

#include 
#include 

namespace {
    void on_stack()
    {
        int i;
    }

    void on_heap()
    {
        int* i = new int;
        delete i;
    }
}

int main()
{
    auto begin = std::chrono::system_clock::now();
    for (int i = 0; i < 1000000000; ++i)
        on_stack();
    auto end = std::chrono::system_clock::now();

    std::printf("on_stack took %f seconds\n", std::chrono::duration(end - begin).count());

    begin = std::chrono::system_clock::now();
    for (int i = 0; i < 1000000000; ++i)
        on_heap();
    end = std::chrono::system_clock::now();

    std::printf("on_heap took %f seconds\n", std::chrono::duration(end - begin).count());
    return 0;
}

显示:

on_stack took 2.070003 seconds
on_heap took 57.980081 seconds

在使用命令行编译时在我的系统上cl foo.cc /Od /MT /EHsc.

您可能不同意我获取非优化构建的方法.这很好:随意修改基准测试.当我打开优化时,我得到:

on_stack took 0.000000 seconds
on_heap took 51.608723 seconds

不是因为堆栈分配实际上是瞬时的,而是因为任何半合适的编译器都可以注意到on_stack它没有做任何有用的事情并且可以被优化掉.我的Linux笔记本电脑上的GCC也注意到on_heap它没有做任何有用的事情,并且也将其优化掉:

on_stack took 0.000003 seconds
on_heap took 0.000002 seconds


摆脱这样的代码是优化器的工作.是否有充分的理由打开优化器然后阻止它实际优化?我编写了答案,使事情变得更加清晰:如果您喜欢与优化器对抗,请准备好了解智能编译器的编写方式.
在没有优化的Windows(调试版本)上,它将使用比非调试堆慢得多的调试堆.我认为根本不"欺骗"优化器并不是一个坏主意.编译器编写者很聪明,但编译器不是AI.
我已经很晚了,但是这里也值得一提的是堆分配通过内核请求内存,因此性能的提升也很大程度上取决于内核的效率.在Linux上使用此代码(Linux 3.10.7-gentoo#2 SMP Wed Sep 4 18:58:21 MDT 2013 x86_64),修改HR计时器,并在每个循环中使用1亿次迭代产生这样的性能:`堆栈分配花了0.15354秒,堆分配花了0.834044秒`设置了`-O0`,使得Linux堆分配在我的特定机器上的速度只有5.5左右.
此外,您应该在主函数的最开始添加一个"校准"循环,以便了解每个循环周期的时间,并调整其他循环以确保您的示例运行一些时间,而不是你正在使用的固定常数.
我也很高兴增加每个选项循环运行的次数(加上指示g ++不优化?)已经取得了显着的成果.所以现在我们有很多事实可以说堆栈更快.谢谢你的努力!

4> Furious Code..:

我在Xbox 360氙处理器上学习堆栈与堆分配的一个有趣的事情,也可能适用于其他多核系统,是在堆上分配导致关键部分被输入以停止所有其他核心,以便alloc不会没有冲突.因此,在紧密循环中,堆栈分配是固定大小的阵列的方法,因为它可以防止停顿.

如果您正在为多核/多进程编码,这可能是另一个要考虑的加速,因为您的堆栈分配只能由运行您的作用域函数的核心查看,并且不会影响任何其他核心/ CPU.


这是堆分配器(特别差)实现的结果.更好的堆分配器不需要在每次分配时获得锁定.
大多数多核机器都是如此,而不仅仅是Xenon.甚至Cell也必须这样做,因为你可能在PPU核心上运行两个硬件线程.

5> Chris Jester..:

您可以为特定大小的对象编写特殊的堆分配器,这些对象非常高效.但是,通用堆分配器不是特别高效.

我同意TorbjörnGyllebring关于物体的预期寿命.好点子!



6> MarkR..:

我不认为堆栈分配和堆分配通常是可互换的.我也希望它们的性能足以满足一般用途.

我强烈推荐小件物品,无论哪个更适合分配范围.对于大型项目,堆可能是必需的.

在具有多个线程的32位操作系统上,堆栈通常相当有限(尽管通常至少为几mb),因为地址空间需要被分割,迟早一个线程堆栈将运行到另一个线程堆栈中.在单线程系统(无论如何都是Linux glibc单线程)上,限制要少得多,因为堆栈可以增长和增长.

在64位操作系统上,有足够的地址空间使线程堆栈非常大.



7> Windows prog..:

通常,堆栈分配只包括从堆栈指针寄存器中减去.这比搜索堆快得多.

有时堆栈分配需要添加一个或多个虚拟内存页面.添加新的归零内存页面不需要从磁盘读取页面,因此通常这比搜索堆快得多(特别是如果堆的一部分也被分页).在极少数情况下,你可以构造这样一个例子,在堆已经在RAM中的部分堆中恰好可以使用足够的空间,但是为堆栈分配新页面必须等待其他页面被写出来到磁盘.在这种罕见的情况下,堆更快.


这是一个例子.调用程序要求分配37个字节.库函数查找至少40个字节的块.空闲列表中的第一个块有16个字节.空闲列表中的第二个块有12个字节.第三个块有44个字节.图书馆在此时停止搜索.

8> 小智..:

除了堆分配的数量级性能优势之外,堆栈分配对于长时间运行的服务器应用程序更为可取.即使是最好的托管堆最终也会变得如此分散,以至于应用程序性能会下降.

推荐阅读
小妖694_807
这个屌丝很懒,什么也没留下!
DevBox开发工具箱 | 专业的在线开发工具网站    京公网安备 11010802040832号  |  京ICP备19059560号-6
Copyright © 1998 - 2020 DevBox.CN. All Rights Reserved devBox.cn 开发工具箱 版权所有