我在2009年首先注意到,如果我对大小(-Os
)而不是速度(-O2
或-O3
)进行优化,那么GCC(至少在我的项目和我的机器上)倾向于生成明显更快的代码,而我一直在想为什么。
我设法创建了(相当愚蠢的)代码来显示这种令人惊讶的行为,并且足够小,可以在此处发布。
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 s来执行此程序,如果使用-O2
或编译,则需要0.44 s的时间-O3
。始终获得这些时间并且几乎没有噪音(gcc 4.7.2,x86_64 GNU / Linux,Intel Core i5-3320M)。
(更新:我已将所有汇编代码移至GitHub:他们使帖子made肿,并且由于fno-align-*
标志具有相同的作用,因此显然对问题的价值很小。)
不幸的是,我对程序集的理解非常有限,所以我不知道接下来的操作是否正确:我抓住了程序集,-O2
并将-Os
除.p2align
行之外的所有差异合并到了程序集中,这里是结果。这段代码仍然可以在0.38s内运行,唯一的不同是 .p2align
东西。
如果我猜对了,这些是用于堆栈对齐的填充。根据为什么GCC垫与NOP一起起作用?这样做是为了希望代码能更快地运行,但是显然,这种优化在我看来是适得其反的。
在这种情况下,罪魁祸首是填充物吗?为什么以及如何?
它产生的噪声使定时微优化变得不可能。
当我对C或C ++源代码进行微优化(与堆栈对齐无关)时,如何确保这种偶然的幸运/不幸的对齐方式不会受到干扰?
更新:
遵循Pascal Cuoq的回答,我对对齐方式进行了一些修改。通过传递-O2 -fno-align-functions -fno-align-loops
给gcc,所有内容.p2align
都从程序集中删除,生成的可执行文件在0.38s内运行。根据gcc文档:
-Os启用所有-O2优化[但是] -Os禁用以下优化标志:
-falign-functions -falign-jumps -falign-loops -falign-labels -freorder-blocks -freorder-blocks-and-partition -fprefetch-loop-arrays
因此,这似乎是一个(错误)对齐问题。
我仍然持怀疑态度,-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.37s 之后手动移动组件-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
而不是上面给出的方法衡量的缓存未命中。缓存未命中非常嘈杂,与执行时间几乎没有关联。