我在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测量的缓存失误,而不仅仅是上面给出的那些。缓存丢失是非常非常嘈杂的,并且与执行时间几乎没有相关性。


当前回答

我绝不是这方面的专家,但我似乎记得,当涉及到分支预测时,现代处理器相当敏感。用于预测分支的算法(至少在我编写汇编程序代码时是这样)基于代码的几个属性,包括目标的距离和方向。

我想到的场景是小循环。当分支向后走并且距离不是太远时,分支预测就会针对这种情况进行优化,因为所有的小循环都是这样完成的。当您在生成的代码中交换add和work的位置时,或者当两者的位置发生轻微变化时,同样的规则可能会发挥作用。

也就是说,我不知道如何验证这一点,我只是想让你知道,这可能是你想要研究的东西。

其他回答

如果您的程序受CODE L1缓存的限制,那么针对大小进行优化就会突然开始发挥作用。

当我最后检查时,编译器还不够聪明,不能在所有情况下都解决这个问题。

在您的例子中,-O3可能生成的代码足够用于两条缓存线,但-Os适合用于一条缓存线。

我绝不是这方面的专家,但我似乎记得,当涉及到分支预测时,现代处理器相当敏感。用于预测分支的算法(至少在我编写汇编程序代码时是这样)基于代码的几个属性,包括目标的距离和方向。

我想到的场景是小循环。当分支向后走并且距离不是太远时,分支预测就会针对这种情况进行优化,因为所有的小循环都是这样完成的。当您在生成的代码中交换add和work的位置时,或者当两者的位置发生轻微变化时,同样的规则可能会发挥作用。

也就是说,我不知道如何验证这一点,我只是想让你知道,这可能是你想要研究的东西。

我的同事帮我找到了一个合理的答案。他注意到了256字节边界的重要性。他没有在这里注册,并鼓励我自己发布答案(并获得所有的名声)。


简短的回答:

在这种情况下,填充物是罪魁祸首吗?为什么?怎么做?

这一切都归结为结盟。对齐可以对性能产生重大影响,这就是为什么我们首先使用-falign-*标志。

我已经向gcc开发人员提交了一份(虚假的?)错误报告。事实证明,默认行为是“我们默认将循环对齐到8字节,但如果我们不需要填充超过10个字节,则尝试将其对齐到16字节。”显然,在这种特殊情况下以及在我的机器上,这种默认设置不是最佳选择。带有-O3的Clang 3.4 (trunk)进行了适当的对齐,生成的代码没有显示这种奇怪的行为。

当然,如果对齐不当,情况会更糟。一个不必要的/糟糕的对齐只会毫无理由地消耗字节,并可能增加缓存丢失等。

它产生的噪音使时间进行了微优化 不可能的。 我如何确保这种偶然的幸运/不幸的对齐 当我做微优化(与堆栈无关 对齐)的C或c++源代码?

简单地告诉gcc做正确的对齐:

g++ -O2 -falign-functions=16 -falign-loops=16


长一点的回答:

如果出现以下情况,代码运行速度会变慢:

一个XX字节的边界在中间切割add() (XX依赖于机器)。 如果对add()的调用必须跳过XX字节边界,并且目标没有对齐。 如果add()没有对齐。 如果循环没有对齐。

前两个在Marat Dukhan好心发布的代码和结果中都很明显。在这种情况下,gcc-4.8.1 -Os(在0.994秒内执行):

00000000004004fd <_ZL3addRKiS0_.isra.0>:
  4004fd:       8d 04 37                lea    eax,[rdi+rsi*1]
  400500:       c3   

256字节的边界将add()切到正中间,并且add()和循环都没有对齐。惊喜,惊喜,这是最慢的情况!

在gcc-4.7.3 -Os(在0.822秒内执行)的情况下,256字节的边界只切割到冷部分(但循环和add()都不会被切割):

00000000004004fa <_ZL3addRKiS0_.isra.0>:
  4004fa:       8d 04 37                lea    eax,[rdi+rsi*1]
  4004fd:       c3                      ret

[...]

  40051a:       e8 db ff ff ff          call   4004fa <_ZL3addRKiS0_.isra.0>

没有任何东西是对齐的,对add()的调用必须跳过256字节的边界。这段代码是第二慢的。

在gcc-4.6.4 -Os(在0.709秒内执行)的情况下,虽然没有任何对齐,但对add()的调用不需要跳过256字节的边界,而目标正好在32字节之外:

  4004f2:       e8 db ff ff ff          call   4004d2 <_ZL3addRKiS0_.isra.0>
  4004f7:       01 c3                   add    ebx,eax
  4004f9:       ff cd                   dec    ebp
  4004fb:       75 ec                   jne    4004e9 <_ZL4workii+0x13>

这是三个中最快的。为什么256字节的边界在他的机器上是特殊的,我将把它留给他去弄清楚。我没有这样的处理器。

现在,在我的机器上,我没有这个256字节的边界效果。在我的机器上,只有函数和循环对齐生效。如果我传递g++ -O2 -falign-functions=16 -falign-loops=16,那么一切都恢复正常:我总是得到最快的情况,时间不再对-fno- ignore -frame-pointer标志敏感。我可以传递g++ -O2 -falign-functions=32 -falign-loops=32或16的任何倍数,代码也不敏感。

我在2009年第一次注意到gcc(至少在我的项目和我的 机器)倾向于生成明显更快的代码 优化的大小(-Os)而不是速度(-O2或-O3),我已经 从那以后就一直在想为什么。

一个可能的解释是,我的热点对对齐很敏感,就像这个例子中的一样。通过打乱标志(传递-Os而不是-O2),这些热点以一种幸运的方式意外对齐,代码变得更快。这与优化大小无关:这些完全是偶然的,热点对齐得更好。从现在开始,我将检查对齐对我的项目的影响。

哦,还有一件事。这样的热点是如何出现的,就像例子中显示的那样?像add()这样的小函数的内联怎么会失败呢?

考虑一下:

// add.cpp
int add(const int& x, const int& y) {
    return x + y;
}

在一个单独的文件中:

// main.cpp
int add(const int& x, const int& y);

const int LOOP_BOUND = 200000000;

__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;
}

编译为:g++ -O2 add.cpp main.cpp。

gcc不会内联add()!

就这样,很容易无意中创建像op中那样的热点。当然,这在一定程度上是我的错:gcc是一个优秀的编译器。如果将上面的代码编译为:g++ -O2 -flto add.cpp main.cpp,也就是说,如果我进行链接时间优化,代码在0.19秒内运行!

(内联在OP中被人为禁用,因此,OP中的代码慢了2倍)。

我认为你可以得到与你所做的相同的结果:

我抓取了-O2的程序集,并将其所有差异合并到-Os的程序集中,除了.p2align行:

-O2 -falign-functions=1 -falign- leaps =1 -falign-loops=1 -falign-labels=115年来,我一直在用这些选项编译所有东西,每次我费心测量时,它们都比普通的-O2快。

另外,对于完全不同的上下文(包括不同的编译器),我注意到情况类似:应该“优化代码大小而不是速度”的选项针对代码大小和速度进行优化。

如果我猜对了,这些是堆栈对齐的填充。

不,这与堆栈无关,默认生成的nop和options -falign-*=1 prevent用于代码对齐。

根据为什么GCC垫功能与NOPs?这样做是希望代码运行得更快,但显然这种优化在我的情况下适得其反。 在这种情况下,填充物是罪魁祸首吗?为什么?怎么做?

It is very likely that the padding is the culprit. The reason padding is felt to be necessary and is useful in some cases is that code is typically fetched in lines of 16 bytes (see Agner Fog's optimization resources for the details, which vary by model of processor). Aligning a function, loop, or label on a 16-bytes boundary means that the chances are statistically increased that one fewer lines will be necessary to contain the function or loop. Obviously, it backfires because these NOPs reduce code density and therefore cache efficiency. In the case of loops and label, the NOPs may even need to be executed once (when execution arrives to the loop/label normally, as opposed to from a jump).

默认情况下,编译器针对“普通”处理器进行优化。由于不同的处理器支持不同的指令序列,通过-O2启用的编译器优化可能会使一般处理器受益,但会降低特定处理器的性能(同样适用于-Os)。如果您在不同的处理器上尝试相同的示例,您会发现其中一些处理器受益于-O2,而其他处理器更有利于-Os优化。

下面是几个处理器上time ./test 0 0的结果(报告用户时间):

Processor (System-on-Chip)             Compiler   Time (-O2)  Time (-Os)  Fastest
AMD Opteron 8350                       gcc-4.8.1    0.704s      0.896s      -O2
AMD FX-6300                            gcc-4.8.1    0.392s      0.340s      -Os
AMD E2-1800                            gcc-4.7.2    0.740s      0.832s      -O2
Intel Xeon E5405                       gcc-4.8.1    0.603s      0.804s      -O2
Intel Xeon E5-2603                     gcc-4.4.7    1.121s      1.122s       -
Intel Core i3-3217U                    gcc-4.6.4    0.709s      0.709s       -
Intel Core i3-3217U                    gcc-4.7.3    0.708s      0.822s      -O2
Intel Core i3-3217U                    gcc-4.8.1    0.708s      0.944s      -O2
Intel Core i7-4770K                    gcc-4.8.1    0.296s      0.288s      -Os
Intel Atom 330                         gcc-4.8.1    2.003s      2.007s      -O2
ARM 1176JZF-S (Broadcom BCM2835)       gcc-4.6.3    3.470s      3.480s      -O2
ARM Cortex-A8 (TI OMAP DM3730)         gcc-4.6.3    2.727s      2.727s       -
ARM Cortex-A9 (TI OMAP 4460)           gcc-4.6.3    1.648s      1.648s       -
ARM Cortex-A9 (Samsung Exynos 4412)    gcc-4.6.3    1.250s      1.250s       -
ARM Cortex-A15 (Samsung Exynos 5250)   gcc-4.7.2    0.700s      0.700s       -
Qualcomm Snapdragon APQ8060A           gcc-4.8       1.53s       1.52s      -Os

在某些情况下,你可以通过让gcc为你的特定处理器进行优化来缓解不利优化的影响(使用选项-mtune=native或-march=native):

Processor            Compiler   Time (-O2 -mtune=native) Time (-Os -mtune=native)
AMD FX-6300          gcc-4.8.1         0.340s                   0.340s
AMD E2-1800          gcc-4.7.2         0.740s                   0.832s
Intel Xeon E5405     gcc-4.8.1         0.603s                   0.803s
Intel Core i7-4770K  gcc-4.8.1         0.296s                   0.288s

更新:在基于Ivy bridge的Core i3上,gcc的三个版本(4.6.4、4.7.3和4.8.1)生成的二进制文件的性能有显著不同,但汇编代码只有细微的变化。到目前为止,我还没有对这个事实做出解释。

gcc-4.6.4 -Os的汇编(在0.709秒内执行):

00000000004004d2 <_ZL3addRKiS0_.isra.0>:
  4004d2:       8d 04 37                lea    eax,[rdi+rsi*1]
  4004d5:       c3                      ret

00000000004004d6 <_ZL4workii>:
  4004d6:       41 55                   push   r13
  4004d8:       41 89 fd                mov    r13d,edi
  4004db:       41 54                   push   r12
  4004dd:       41 89 f4                mov    r12d,esi
  4004e0:       55                      push   rbp
  4004e1:       bd 00 c2 eb 0b          mov    ebp,0xbebc200
  4004e6:       53                      push   rbx
  4004e7:       31 db                   xor    ebx,ebx
  4004e9:       41 8d 34 1c             lea    esi,[r12+rbx*1]
  4004ed:       41 8d 7c 1d 00          lea    edi,[r13+rbx*1+0x0]
  4004f2:       e8 db ff ff ff          call   4004d2 <_ZL3addRKiS0_.isra.0>
  4004f7:       01 c3                   add    ebx,eax
  4004f9:       ff cd                   dec    ebp
  4004fb:       75 ec                   jne    4004e9 <_ZL4workii+0x13>
  4004fd:       89 d8                   mov    eax,ebx
  4004ff:       5b                      pop    rbx
  400500:       5d                      pop    rbp
  400501:       41 5c                   pop    r12
  400503:       41 5d                   pop    r13
  400505:       c3                      ret

gcc-4.7.3 -Os的程序集(在0.822秒内执行):

00000000004004fa <_ZL3addRKiS0_.isra.0>:
  4004fa:       8d 04 37                lea    eax,[rdi+rsi*1]
  4004fd:       c3                      ret

00000000004004fe <_ZL4workii>:
  4004fe:       41 55                   push   r13
  400500:       41 89 f5                mov    r13d,esi
  400503:       41 54                   push   r12
  400505:       41 89 fc                mov    r12d,edi
  400508:       55                      push   rbp
  400509:       bd 00 c2 eb 0b          mov    ebp,0xbebc200
  40050e:       53                      push   rbx
  40050f:       31 db                   xor    ebx,ebx
  400511:       41 8d 74 1d 00          lea    esi,[r13+rbx*1+0x0]
  400516:       41 8d 3c 1c             lea    edi,[r12+rbx*1]
  40051a:       e8 db ff ff ff          call   4004fa <_ZL3addRKiS0_.isra.0>
  40051f:       01 c3                   add    ebx,eax
  400521:       ff cd                   dec    ebp
  400523:       75 ec                   jne    400511 <_ZL4workii+0x13>
  400525:       89 d8                   mov    eax,ebx
  400527:       5b                      pop    rbx
  400528:       5d                      pop    rbp
  400529:       41 5c                   pop    r12
  40052b:       41 5d                   pop    r13
  40052d:       c3                      ret

gcc-4.8.1 -Os中的程序集(在0.994秒内执行):

00000000004004fd <_ZL3addRKiS0_.isra.0>:
  4004fd:       8d 04 37                lea    eax,[rdi+rsi*1]
  400500:       c3                      ret

0000000000400501 <_ZL4workii>:
  400501:       41 55                   push   r13
  400503:       41 89 f5                mov    r13d,esi
  400506:       41 54                   push   r12
  400508:       41 89 fc                mov    r12d,edi
  40050b:       55                      push   rbp
  40050c:       bd 00 c2 eb 0b          mov    ebp,0xbebc200
  400511:       53                      push   rbx
  400512:       31 db                   xor    ebx,ebx
  400514:       41 8d 74 1d 00          lea    esi,[r13+rbx*1+0x0]
  400519:       41 8d 3c 1c             lea    edi,[r12+rbx*1]
  40051d:       e8 db ff ff ff          call   4004fd <_ZL3addRKiS0_.isra.0>
  400522:       01 c3                   add    ebx,eax
  400524:       ff cd                   dec    ebp
  400526:       75 ec                   jne    400514 <_ZL4workii+0x13>
  400528:       89 d8                   mov    eax,ebx
  40052a:       5b                      pop    rbx
  40052b:       5d                      pop    rbp
  40052c:       41 5c                   pop    r12
  40052e:       41 5d                   pop    r13
  400530:       c3                      ret