我在 2009 年第一次注意到,如果我针对大小(-Os
)而不是速度(-O2
或)进行优化,GCC(至少在我的项目和机器上)倾向于生成明显更快的代码-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
或编译它需要 0.44 秒-O3
。这些时间是一致获得的,几乎没有噪音(gcc 4.7.2、x86_64 GNU/Linux、Intel Core i5-3320M)。
(更新:我已将所有汇编代码移至GitHub:他们使帖子变得臃肿,并且显然对问题没有什么价值,因为fno-align-*
标志具有相同的效果。)
不幸的是,我对汇编的理解非常有限,所以我不知道我接下来所做的是否正确:我抓住了汇编,-O2
并将其所有差异合并到汇编中,-Os
除了行.p2align
,结果在这里。这段代码仍然在 0.38 秒内运行,唯一的区别是 .p2align
东西。
如果我猜对了,这些是堆栈对齐的填充。根据Why does GCC pad functions with 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.37s-O2 -fno-align-functions -fno-align-loops
0.37s-S -O2
然后手动移动0.37sadd()
后的组件work()
-O2
0.44s
在我看来,add()
与呼叫站点的距离很重要。我已经尝试过perf
,但是 and 的输出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
,而不仅仅是上面给出的那些。缓存未命中非常嘈杂,与执行时间几乎没有相关性。