我在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-*标志具有相同的效果。)
下面是生成的带有-Os和-O2的程序集。
不幸的是,我对汇编的理解非常有限,所以我不知道我接下来所做的是否正确:我抓取了-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-跳转-falign-循环 -falign-labels -freorder-blocks -freorder-blocks-and-partition -fprefetch-loop-arrays
所以,这似乎是一个(错误的)对齐问题。
我仍然对Marat Dukhan的回答中提出的-march=native表示怀疑。我不相信它不只是干扰这个(错误的)对齐问题;这对我的机器完全没有影响。(不过,我还是对他的回答投了赞成票。)
更新2:
我们可以把- o去掉。下面的时间是通过编译
-O2 -fno-省略帧指针0.37秒 -O2 -fno-align-functions -fno-align-loops 0.37s -S -O2然后手动移动组装add()后工作()0.37s - 02 0.44秒
在我看来,add()到调用站点的距离很重要。我已经尝试了性能,但性能统计和性能报告的输出对我来说意义不大。然而,我只能得到一个一致的结果:
-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()的调用上出现了延迟。
我已经检查了我的机器上能吐出的一切东西;不仅仅是上面给出的数据。
对于同一可执行文件,暂停周期前沿与执行时间呈线性相关;我没有注意到任何其他与之如此明显相关的东西。(比较不同可执行文件的stopped -cycles-frontend对我来说没有意义。)
我在第一个注释出现时就包含了缓存错误。我检查了所有可以在我的机器上通过perf测量的缓存失误,而不仅仅是上面给出的那些。缓存丢失是非常非常嘈杂的,并且与执行时间几乎没有相关性。