当我启用C 11时,我在一个小的C片段中发现了一个有趣的性能回归:
#include <vector>
struct Item
{
int a;
int b;
};
int main()
{
const std::size_t num_items = 10000000;
std::vector<Item> container;
container.reserve(num_items);
for (std::size_t i = 0; i < num_items; ++i) {
container.push_back(Item());
}
return 0;
}
随着g(GCC)4.8.2 20131219(预发行)和C 03我得到:
milian:/tmp$ g++ -O3 main.cpp && perf stat -r 10 ./a.out
Performance counter stats for './a.out' (10 runs):
35.206824 task-clock # 0.988 CPUs utilized ( +- 1.23% )
4 context-switches # 0.116 K/sec ( +- 4.38% )
0 cpu-migrations # 0.006 K/sec ( +- 66.67% )
849 page-faults # 0.024 M/sec ( +- 6.02% )
95,693,808 cycles # 2.718 GHz ( +- 1.14% ) [49.72%]
<not supported> stalled-cycles-frontend
<not supported> stalled-cycles-backend
95,282,359 instructions # 1.00 insns per cycle ( +- 0.65% ) [75.27%]
30,104,021 branches # 855.062 M/sec ( +- 0.87% ) [77.46%]
6,038 branch-misses # 0.02% of all branches ( +- 25.73% ) [75.53%]
0.035648729 seconds time elapsed ( +- 1.22% )
另一方面,在启用C 11的情况下,性能会显着下降:
milian:/tmp$ g++ -std=c++11 -O3 main.cpp && perf stat -r 10 ./a.out
Performance counter stats for './a.out' (10 runs):
86.485313 task-clock # 0.994 CPUs utilized ( +- 0.50% )
9 context-switches # 0.104 K/sec ( +- 1.66% )
2 cpu-migrations # 0.017 K/sec ( +- 26.76% )
798 page-faults # 0.009 M/sec ( +- 8.54% )
237,982,690 cycles # 2.752 GHz ( +- 0.41% ) [51.32%]
<not supported> stalled-cycles-frontend
<not supported> stalled-cycles-backend
135,730,319 instructions # 0.57 insns per cycle ( +- 0.32% ) [75.77%]
30,880,156 branches # 357.057 M/sec ( +- 0.25% ) [75.76%]
4,188 branch-misses # 0.01% of all branches ( +- 7.59% ) [74.08%]
0.087016724 seconds time elapsed ( +- 0.50% )
有人可以解释一下吗?到目前为止,我的经验是,通过启用C 11,特别是STL变得更快 . 感谢移动语义 .
EDIT: 正如所建议的那样,使用 container.emplace_back();
而不是性能与C 03版本相当 . C 03版本如何实现 push_back
的相同功能?
milian:/tmp$ g++ -std=c++11 -O3 main.cpp && perf stat -r 10 ./a.out
Performance counter stats for './a.out' (10 runs):
36.229348 task-clock # 0.988 CPUs utilized ( +- 0.81% )
4 context-switches # 0.116 K/sec ( +- 3.17% )
1 cpu-migrations # 0.017 K/sec ( +- 36.85% )
798 page-faults # 0.022 M/sec ( +- 8.54% )
94,488,818 cycles # 2.608 GHz ( +- 1.11% ) [50.44%]
<not supported> stalled-cycles-frontend
<not supported> stalled-cycles-backend
94,851,411 instructions # 1.00 insns per cycle ( +- 0.98% ) [75.22%]
30,468,562 branches # 840.991 M/sec ( +- 1.07% ) [76.71%]
2,723 branch-misses # 0.01% of all branches ( +- 9.84% ) [74.81%]
0.036678068 seconds time elapsed ( +- 0.80% )
1 回答
我可以使用您在帖子中写下的选项在我的机器上重现您的结果 .
However, if I also enable link time optimization (I also pass the -flto flag to gcc 4.7.2), the results are identical:
(我正在使用
container.push_back(Item());
编译您的原始代码)至于原因,需要查看生成的汇编代码(
g++ -std=c++11 -O3 -S regr.cpp
) . In C++11 mode the generated code is significantly more cluttered 比C 98模式和 inlining the functionvoid std::vector<Item,std::allocator<Item>>::_M_emplace_back_aux<Item>(Item&&)
fails 在C 11模式下,默认为
inline-limit
.This failed inline has a domino effect. 不是因为正在调用此函数(它甚至没有调用!),而是因为我们必须做好准备:如果调用它,函数argments(
Item.a
和Item.b
)必须已经在正确的位置 . 这会导致代码混乱 .以下是 inlining succeeds 所生成代码的相关部分:
这是一个很好的紧凑的循环 . 现在,让我们将其与 failed inline 案例进行比较:
这段代码杂乱无章,循环中的内容比前一种情况要多得多 . 在函数
call
(显示最后一行)之前,必须正确放置参数:即使从未实际执行过,循环也会在之前排列:
This leads to the messy code. 如果由于内联成功没有函数
call
,我们在循环中只有2个移动指令,并且%rsp
(堆栈指针)没有混乱 . 但是,如果内联失败,我们会得到6个动作,而且我们会在%rsp
中乱七八糟 .只是为了证实我的理论(注意
-finline-limit
),在C 11模式下:Indeed, if we ask the compiler to try just a little bit harder to inline that function, the difference in performance goes away.
那么从这个故事中拿走了什么呢?内联失败可能会花费你很多,你应该充分利用编译器功能: I can only recommend link time optimization. 它为我的程序提供了显着的性能提升(高达2.5倍),我需要做的就是传递
-flto
标志 . 这是一个非常好的交易! ;)但是,我不建议使用inline关键字删除代码;让编译器决定做什么 . (无论如何,优化器都可以将内联关键字视为空格 . )
好问题,1!