哪个更快:堆栈分配或堆分配
这个问题听起来相当基本,但这是我与另一位与我合作的开发人员进行的辩论。
我正在考虑堆栈分配的东西,而不是分配给他们。 他正在跟我说话,看着我的肩膀,并评论说这不是必要的,因为他们在表现上同样明智。
我始终认为,堆栈的增长时间是固定的,堆分配的性能取决于堆的当前复杂性,以便分配(找到适当大小的空洞)和解除分配(减少空洞以减少碎片),就像如果我没有弄错,很多标准库实现在删除期间需要时间来执行此操作)。
这让我觉得这可能是非常依赖编译器的东西。 特别是对于这个项目,我使用了一个Metrowerks编译器来实现PPC体系结构。 对这种组合的洞察将是最有帮助的,但总的来说,对于GCC和MSVC ++来说,情况如何? 堆分配不像堆栈分配那样高性能吗? 没有区别吗? 或者差异如此之小,它变得毫无意义的微观优化。
堆栈分配速度要快得多,因为它确实要做的就是移动堆栈指针。 使用内存池,您可以从堆分配中获得可比较的性能,但这会带来轻微的复杂性和自身的问题。
堆栈与堆栈不仅仅是性能的考虑因素; 它还会告诉你很多关于对象的预期寿命。
堆栈要快得多。 在大多数情况下,它在大多数体系结构中仅使用单一指令,例如在x86上:
sub esp, 0x10
(这将栈指针向下移动0x10字节,从而“分配”这些字节以供变量使用。)
当然,堆栈的大小非常非常有限,因为您会很快发现是否过度使用堆栈分配或尝试执行递归操作:-)
此外,几乎没有理由优化不需要验证的代码的性能,如分析所证明的那样。 “不成熟的优化”通常会导致更多的问题,而不是它的价值。
我的经验法则:如果我知道在编译时需要一些数据,并且它的大小在几百字节以内,那么我会对它进行堆栈分配。 否则,我会堆分配它。
老实说,编写一个程序来比较性能是微不足道的:
#include <ctime>
#include <iostream>
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 ticksn";
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 ticksn";
}
据说愚蠢的一致性是小心灵的大地精。 显然,优化编译器是许多程序员头脑中的好伙伴。 这个讨论曾经是答案的底部,但是人们显然不会读到那么远,所以我在这里提出来避免提出我已经回答的问题。
一个优化编译器可能会注意到这段代码什么都不做,并且可能会优化它。 优化者的工作就是做这样的事情,与优化器作斗争是一件愚蠢的事情。
我建议编译这个代码时关闭优化,因为没有好的方法来欺骗当前正在使用的或将来会使用的每个优化器。
任何打开优化器然后抱怨打击它的人都应该受到公众的嘲笑。
如果我关心纳秒级精度,我不会使用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 ++不会隐藏构造函数。
在我写这篇文章之后的几年中,堆栈溢出的优先选择是从优化版本发布性能。 总的来说,我认为这是正确的。 不过,我仍然认为当你实际上不希望代码优化时,要求编译器优化代码是很愚蠢的。 这让我感到非常相似,为代客停车付出额外费用,但拒绝交出钥匙。 在这种特殊情况下,我不想让优化器运行。
使用稍微修改过的基准测试版(以解决原始程序每次通过循环时不会在堆栈上分配某些内容的有效点),并且无需进行优化即可编译,而是链接到发布库(以解决我们的有效问题不想包含链接到调试库引起的任何放缓):
#include <cstdio>
#include <chrono>
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 secondsn", std::chrono::duration<double>(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 secondsn", std::chrono::duration<double>(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
链接地址: http://www.djcxy.com/p/243.html