老实说,编写一个程序来比较性能是微不足道的:
#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 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 时钟滴答”,即使这样,堆栈分配和“15 个时钟滴答”也得到“0 时钟滴答” " 用于堆分配。当我测量 10,000,000 个分配时,堆栈分配需要 31 个时钟滴答,而堆分配需要 1562 个时钟滴答。
是的,优化编译器可能会省略创建空对象。如果我理解正确,它甚至可能会忽略整个第一个循环。当我将迭代次数增加到 10,000,000 次时,堆栈分配需要 31 个时钟滴答,而堆分配需要 1562 个时钟滴答。我认为可以肯定地说,在没有告诉 g++ 优化可执行文件的情况下,g++ 没有省略构造函数。
自从我写这篇文章以来的几年里,Stack Overflow 的偏好一直是通过优化构建发布性能。总的来说,我认为这是正确的。但是,我仍然认为当您实际上不希望代码优化时要求编译器优化代码是愚蠢的。我觉得这与为代客泊车支付额外费用非常相似,但拒绝交出钥匙。在这种特殊情况下,我不希望优化器运行。
使用稍微修改过的基准测试版本(解决原始程序每次通过循环都没有在堆栈上分配东西的有效点)并在没有优化但链接到发布库的情况下进行编译(解决我们没有的有效点'不想包括因链接到调试库而导致的任何减速):
#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 seconds\n", 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 seconds\n", 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