这个问题可能听起来相当简单,但这是我与另一位与我合作的开发人员的辩论.
我正在小心处理堆栈分配的东西,而不是堆分配它们.他正在跟我说话,看着我的肩膀并评论说这没有必要,因为他们的表现是明智的.
我一直认为堆栈的增长是恒定的时间,并且堆分配的性能取决于堆的当前复杂性(用于找到合适大小的孔)和解除分配(折叠孔以减少碎片,如如果我没有弄错的话,许多标准库实现在删除期间需要时间来完成此操作.
这让我觉得可能非常依赖于编译器.特别是对于这个项目,我使用Metrowerks编译器来实现PPC架构.对这种组合的洞察力将是最有帮助的,但总的来说,对于GCC和MSVC++,情况如何?堆分配不如堆栈分配高吗?没有区别吗?或者差异是如此微小,它变得毫无意义的微优化.
堆栈分配要快得多,因为它真正做的就是移动堆栈指针.使用内存池,您可以从堆分配中获得可比较的性能,但这会带来轻微的复杂性和自身的麻烦.
此外,堆栈与堆不仅是性能考虑因素; 它还告诉你很多关于对象的预期寿命.
堆栈速度更快.在大多数情况下,例如在x86上,它实际上只在大多数体系结构上使用单个指令:
sub esp, 0x10
(这会将堆栈指针向下移动0x10字节,从而"分配"这些字节以供变量使用.)
当然,堆栈的大小非常非常有限,因为您将很快发现是否过度使用堆栈分配或尝试进行递归:-)
此外,没有理由优化不可验证地需要它的代码的性能,例如通过分析证明."过早优化"通常会导致更多问题,而不是它的价值.
我的经验法则:如果我知道我将在编译时需要一些数据,并且它的大小在几百字节之内,我会进行堆栈分配.否则我堆分配它.
老实说,编写一个比较性能的程序是微不足道的:
#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
我在Xbox 360氙处理器上学习堆栈与堆分配的一个有趣的事情,也可能适用于其他多核系统,是在堆上分配导致关键部分被输入以停止所有其他核心,以便alloc不会没有冲突.因此,在紧密循环中,堆栈分配是固定大小的阵列的方法,因为它可以防止停顿.
如果您正在为多核/多进程编码,这可能是另一个要考虑的加速,因为您的堆栈分配只能由运行您的作用域函数的核心查看,并且不会影响任何其他核心/ CPU.
您可以为特定大小的对象编写特殊的堆分配器,这些对象非常高效.但是,通用堆分配器不是特别高效.
我同意TorbjörnGyllebring关于物体的预期寿命.好点子!
我不认为堆栈分配和堆分配通常是可互换的.我也希望它们的性能足以满足一般用途.
我强烈推荐小件物品,无论哪个更适合分配范围.对于大型项目,堆可能是必需的.
在具有多个线程的32位操作系统上,堆栈通常相当有限(尽管通常至少为几mb),因为地址空间需要被分割,迟早一个线程堆栈将运行到另一个线程堆栈中.在单线程系统(无论如何都是Linux glibc单线程)上,限制要少得多,因为堆栈可以增长和增长.
在64位操作系统上,有足够的地址空间使线程堆栈非常大.
通常,堆栈分配只包括从堆栈指针寄存器中减去.这比搜索堆快得多.
有时堆栈分配需要添加一个或多个虚拟内存页面.添加新的归零内存页面不需要从磁盘读取页面,因此通常这比搜索堆快得多(特别是如果堆的一部分也被分页).在极少数情况下,你可以构造这样一个例子,在堆已经在RAM中的部分堆中恰好可以使用足够的空间,但是为堆栈分配新页面必须等待其他页面被写出来到磁盘.在这种罕见的情况下,堆更快.
除了堆分配的数量级性能优势之外,堆栈分配对于长时间运行的服务器应用程序更为可取.即使是最好的托管堆最终也会变得如此分散,以至于应用程序性能会下降.