我在2009年第一次注意到GCC(至少在我的项目和机器上)如果我优化大小 (-Os
)而不是速度(-O2
或-O3
),就倾向于生成明显更快的代码,从那时起我就一直在想为什么。
我已经设法创建了(相当愚蠢的)代码来显示这种令人惊讶的行为,并且足够小,可以在这里发布。
const int LOOP_BOUND = 200000000;
__attribute__((noinline))
static int add(const int& x, const int& y) {
return x + y;
}
__attribute__((noinline))
static int work(int xval, int yval) {
int sum(0);
for (int i=0; i<LOOP_BOUND; ++i) {
int x(xval+sum);
int y(yval+sum);
int z = add(x, y);
sum += z;
}
return sum;
}
int main(int , char* argv[]) {
int result = work(*argv[1], *argv[2]);
return result;
}
如果我用-Os
编译它,执行这个程序需要0.38秒,如果用-O2
或-O3
编译,则需要0.44秒。这些时间是一致的,几乎没有噪音(gcc 4.7.2, x86_64 GNU/Linux, Intel Core i5-3320M)。
(更新:我已经将所有汇编代码移动到GitHub:他们使帖子臃肿,显然对问题增加的价值很小,因为fno-align-*
标志具有相同的效果。)
不幸的是,我对汇编的理解非常有限,所以我不知道我接下来所做的是否正确:我抓取了-O2
的汇编,并将其所有差异合并到-Os
除了的汇编中,.p2align
行,结果在这里。此代码仍然运行在0.38秒和唯一的区别是 .p2align
的东西。
如果我猜对了,这些是堆栈对齐的填充。根据为什么GCC衬垫与NOPs一起工作?,这样做是希望代码运行得更快,但显然这种优化在我的情况下适得其反。
在这种情况下,填充物是罪魁祸首吗?为什么?怎么做?
它所产生的噪音几乎使计时微优化成为不可能。
当我在C或c++源代码上进行微优化(与堆栈对齐无关)时,我如何确保这种意外的幸运/不幸运的对齐不会干扰?
更新:
在Pascal Cuoq的回答之后,我修补了一点对齐。通过将-O2 -fno-align-functions -fno-align-loops
传递给gcc,所有.p2align
都从程序集中消失,生成的可执行文件在0.38秒内运行。根据gcc文档:
-Os启用所有-O2优化[但]-Os禁用以下优化标志:
-falign-functions -falign-jumps -falign-loops -falign-labels -freorder-blocks -freorder-blocks-and-partition -fprefetch-loop-arrays
所以,这似乎是一个(错误的)对齐问题。
我仍然怀疑在Marat Dukhan的回答中建议的-march=native
。我不相信它不只是干扰这个(错误的)对齐问题;这对我的机器完全没有影响。(不过,我还是对他的回答投了赞成票。)
更新2:
我们可以把-Os
排除在外。通过编译
-O2 -fno-omit-frame-pointer
0.37秒
-O2 -fno-align-functions -fno-align-loops
0.37秒
-S -O2
然后手动移动add()
的组装后,work()
0.37秒
-O2
0.44秒
在我看来,add()
到调用站点的距离很重要。我已经尝试过perf
,但perf stat
和perf report
的输出对我来说意义不大。然而,我只能得到一个一致的结果:
-O2
:
602,312,864 stalled-cycles-frontend # 0.00% frontend cycles idle
3,318 cache-misses
0.432703993 seconds time elapsed
[...]
81.23% a.out a.out [.] work(int, int)
18.50% a.out a.out [.] add(int const&, int const&) [clone .isra.0]
[...]
¦ __attribute__((noinline))
¦ static int add(const int& x, const int& y) {
¦ return x + y;
100.00 ¦ lea (%rdi,%rsi,1),%eax
¦ }
¦ ? retq
[...]
¦ int z = add(x, y);
1.93 ¦ ? callq add(int const&, int const&) [clone .isra.0]
¦ sum += z;
79.79 ¦ add %eax,%ebx
fno-align-*
:
604,072,552 stalled-cycles-frontend # 0.00% frontend cycles idle
9,508 cache-misses
0.375681928 seconds time elapsed
[...]
82.58% a.out a.out [.] work(int, int)
16.83% a.out a.out [.] add(int const&, int const&) [clone .isra.0]
[...]
¦ __attribute__((noinline))
¦ static int add(const int& x, const int& y) {
¦ return x + y;
51.59 ¦ lea (%rdi,%rsi,1),%eax
¦ }
[...]
¦ __attribute__((noinline))
¦ static int work(int xval, int yval) {
¦ int sum(0);
¦ for (int i=0; i<LOOP_BOUND; ++i) {
¦ int x(xval+sum);
8.20 ¦ lea 0x0(%r13,%rbx,1),%edi
¦ int y(yval+sum);
¦ int z = add(x, y);
35.34 ¦ ? callq add(int const&, int const&) [clone .isra.0]
¦ sum += z;
39.48 ¦ add %eax,%ebx
¦ }
-fno-omit-frame-pointer
:
404,625,639 stalled-cycles-frontend # 0.00% frontend cycles idle
10,514 cache-misses
0.375445137 seconds time elapsed
[...]
75.35% a.out a.out [.] add(int const&, int const&) [clone .isra.0] ¦
24.46% a.out a.out [.] work(int, int)
[...]
¦ __attribute__((noinline))
¦ static int add(const int& x, const int& y) {
18.67 ¦ push %rbp
¦ return x + y;
18.49 ¦ lea (%rdi,%rsi,1),%eax
¦ const int LOOP_BOUND = 200000000;
¦
¦ __attribute__((noinline))
¦ static int add(const int& x, const int& y) {
¦ mov %rsp,%rbp
¦ return x + y;
¦ }
12.71 ¦ pop %rbp
¦ ? retq
[...]
¦ int z = add(x, y);
¦ ? callq add(int const&, int const&) [clone .isra.0]
¦ sum += z;
29.83 ¦ add %eax,%ebx
在慢速的情况下,我们似乎在对add()
的调用上出现了延迟。
我已经检查了perf -e
可以在我的机器上吐出的一切;不仅仅是上面给出的数据。
对于相同的可执行文件,stalled-cycles-frontend
显示与执行时间线性相关;我没有注意到任何其他与之如此明显相关的东西。(比较不同可执行文件的stalled-cycles-frontend
对我来说没有意义。)
我在第一个注释出现时就包含了缓存错误。我检查了我的机器上可以通过perf
测量的所有缓存失误,而不仅仅是上面给出的那些。缓存丢失是非常非常嘈杂的,并且与执行时间几乎没有相关性。