我第一次注意到 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 s 来执行此程序和 0.44 如果用编译的 s -O2-O3这些时间都获得了持续和几乎无噪音 (gcc 4.7.2,x86_64 GNU/Linux,英特尔酷睿 i5-3320 米)。

(更新︰ 我已移到GitHub的所有程序集代码︰ 它们进行开机自检臃肿,显然很少为增值的问题,作为fno-align-*标志具有相同的效果。)

使用-Os生成的程序集和-O2遗憾的是,我对程序集的了解是非常有限的因此我也不知道是否我接下来做的就是正确︰ 我抓取的程序集-O2和所有差异都合并的程序集-Os .p2align行,结果在此处此代码仍将运行在 0.38s年和的唯一区别是 .p2align 东西。

如果我的推测正确,它们对于堆栈对齐方式填充。根据为什么 GCC 板功能与 NOPs?并没有侥幸的代码将运行得更快,但显然这种优化 backfired 我的情况而言。

它是在这种情况下是罪魁祸首的空白?为什么和如何?

噪音使几乎无法进行计时微优化。

如何确保我做微-优化 (堆栈对齐方式与无关) 在 C 或 c + + 源代码时不影响此类意外的幸运 / unlucky 对齐方式?


更新︰

按照Pascal Cuoq 答案我 tinkered 有点与对齐方式。通过-O2 -fno-align-functions -fno-align-loops到 gcc,所有.p2align都已从该程序集,并生成可执行文件运行在 0.38s年。根据gcc 文档:

-Os 启用所有-O2 优化 [但]-Os 禁用优化以下标志︰

  -falign-functions  -falign-jumps  -falign-loops <br/>
  -falign-labels  -freorder-blocks  -freorder-blocks-and-partition <br/>
  -fprefetch-loop-arrays <br/>

因此,很多似乎 (mis) 对齐问题。

我仍然怀疑-march=native按照建议Marat Dukhan回答。我并不确信它不只干扰此 (mis) 对齐方式问题;它具有绝对不会影响我的计算机。(不过,我 upvoted 他答案。)


更新 2:

我们可以-Os出图片。通过使用进行编译会获得下列时间

  • -O2 -fno-omit-frame-pointer0.37s年

  • -O2 -fno-align-functions -fno-align-loops0.37s年

  • -S -O2然后手动移动的add() work() 0.37s 后的程序集

  • -O2 0.44s年

它就像我的add()的距离从调用站点问题很多。我已经试过perf,但是输出perf statperf 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,而不仅仅是上述可测量的所有缓存未命中。缓存未命中数非常非常嘈杂,并且显示没有关联的执行时间很少。

2013-10-19 20:36:16
问题评论:

失明的猜测︰ 这是否可以为缓存未命中?

@H2CO3 是我的第一也认为,但没有足够的鼓励,不阅读和理解中深度的 OP 的问题的情况下发布注释。

这就是为什么 @g-makulik 我警告,它是"盲推测";-)"TL; 灾难恢复"保留为不正确的问题。: P

只是一个有趣的数据点︰ 我发现-O3 或-Ofast 是大约 1.5 倍快-Os 时我使用上 (我还没有尝试过使用 gcc 重现。) 的操作系统 X.clang 编译这

它是相同的代码。我们的地址在仔细看。L3,对准分支目标非常昂贵。

回答:

默认情况下编译器优化处理器"平均"。因为不同的处理器,请优先考虑不同指令序列,通过启用编译器优化-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来优化您的处理器来缓解 disadvantageous 优化的效果 (使用选项-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

更新︰ 常青藤桥基于核心上 i3 三个版本的gcc4.6.44.7.34.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

只是为了清楚地︰ 未实际并衡量在 12 个不同的平台上操作的代码的性能?(为做的单纯想法的 + 1)

@anatolyg 是,我这么做了 !(并将很快添加其他)

确实。不仅 theorizing 关于不同的 Cpu,但实际证明它为另一个 + 1。我们 (唉) 您看到有关速度每个答案中。与相同的操作系统运行这些测试?(如有可能这扭曲的结果...)

@ 阿里在 AMD FX 6300 -O2 -fno-align-functions -fno-align-loops降到0.340s,时间,使它无法解释的对齐方式。但是,最佳对齐方式是处理器无关︰ 一些处理器更喜欢对齐的循环和函数。

@Jongware 我看不到操作系统会大大影响结果;循环不会进行系统调用。

我的同事帮我找到我的问题貌似合理的答案。他注意到 256 字节的边界的重要性。他未在此处注册,并鼓励我自己张贴答案 (并采取所有堂影响)。


简短的回答︰

它是在这种情况下是罪魁祸首的空白?为什么和如何?

它归结为对齐。对齐方式可以产生重大影响的性能,我们使用在-falign-*标志的初衷。

我已提交给 gcc 的开发人员 (虚假)? bug 报告事实证明的默认行为是"我们将循环到默认的 8 字节对齐但对齐到 16 字节,如果我们不需要在超过 10 个字节填充"。显然,此默认设置不是最佳的选择,在此特定情况下,我的计算机上。Clang 3.4 (干线) 与-O3不适当的对齐方式,并对生成的代码并不显示这种怪现象。

当然,完成不适当的对齐方式,如果它使事情更糟。不必要的 / 坏的对齐方式只是无端耗费字节和缓存未命中数等可能会增加。

噪音使几乎无法进行计时微优化。

如何确保我做微-优化 (堆栈对齐方式与无关) 的 C 或 c + + 源代码代码时不影响此类意外的幸运 / unlucky 对齐方式?

只需通过告诉 gcc 为右对齐方式︰

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


长时间回答︰

该代码将运行慢,如果︰

  • XX字节边界剪切中间 (XX正依赖于计算机) 中的add()

  • 如果对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 字节的边界是在自己计算机上的 speacial,我将离开它直到他算出。没有此类处理器。

现在,我的计算机上没有看到此 256 字节的边界效果。只有函数和循环对齐方式开始我的计算机上。如果我通过g++ -O2 -falign-functions=16 -falign-loops=16然后一切都回正常︰ 我总是获得最快的情况下,时间再也不太敏感的-fno-omit-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()!

这就是所有,就是这么简单,unintendedly 创建如 OP.中所示的热点当然这部分是我的错︰ gcc 是极好的编译器。如果编译上面为︰ g++ -O2 -flto add.cpp main.cpp,即如果我执行链接时优化,代码将运行在 0.19s年 !

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

哇...这确实超出了我通常做什么才能绕过标杆管理异常。

@Ali 我想意义如何编译器内联以来东西它看不到吗?这可能是为什么我们使用inline+ 函数定义标头中。不确定如何成熟 lto 是 gcc。根据我的经验用它至少在 mingw 是命中或错过。

事后,这也就是 @greatwolf 是。:)我相信英特尔 c + + 编译器,Visual Studio 编译器可执行链接时优化。后者是被调用的链接时间代码生成,虽然我的年龄没有使用 Windows。

我认为,这是几年前有关运行非常大的应用程序有一篇文章 ACM 的通讯 (perl,趣味等) 同时移动整个二进制图像一个字节每次使用不同大小的 Linux 环境。我记得 15%左右的典型的差异。其摘要是基准测试结果多是无用,因为对齐此外部变量不予以考虑。

@Gene Interesting,并了解良好 !如果您无法搜集纸,会很好,谢谢 !

我添加这后接受指出︰ 对总体性能的程序-包括大冲突的对齐方式的影响学习了。例如,这篇文章(而我相信这也却在 CACM 版本) 显示链接顺序和 OS 环境大小的变化就足以明显转移性能。以为这样"热循环"的对齐方式。

本白皮书,标题为"不执行任何明显的错误操作产生错误的数据"! 说无意实验偏差程序可能运行环境中的几乎无法控制差异呈现许多基准检验结果毫无意义。

我认为您遇到相同观测到不同角度。

对于性能关键代码,这是对于评估环境在安装或运行时间并选择本地最好的关键例程以不同的方式优化版本之间的系统相当好用的参数。

我认为您可以获取与您的做相同的结果︰

我使用该程序集以-O2 和合并到除.p2align 行-Os 的程序集的所有差异︰

通过使用... -O2 -falign-functions=1 -falign-jumps=1 -falign-loops=1 -falign-labels=1我已被编译所有与这些选项,都比纯快-O2每次我懒得测量,为 15 年。

同时,为完全不同的上下文 (包括不同的编译器),我注意到的情况与此类似︰ 代码大小和速度的优化应该优化代码的大小,而不是速度的选项。

如果我的推测正确,它们对于堆栈对齐方式填充。

这不,有无操作可执行的堆栈中,默认情况下生成和所选项-falign NOPs-* = 1 防止用于代码对齐方式。

GCC 为什么根据板与 NOPs 函数?这样做是希望该代码将运行得更快,但显然这种优化 backfired 我的情况而言。

它是在这种情况下是罪魁祸首的空白?为什么和如何?

空白是罪魁祸首很可能是。填充都可以感觉到,有必要和在某些情况下非常有用的原因是代码通常会读取行 (请参阅有关详细信息,因处理器的型号Agner 模糊优化资源) 的 16 个字节中。对齐功能、 循环或 16 字节边界上的标签意味着机会统计上增加一个更少的行需要包含函数或循环。很明显,它 backfires 因为这些 NOPs 减少代码密度,因此缓存的效率。对于循环和标签,NOPs 可能甚至需要执行一次 (当执行正常情况下,而不是从一个跳转到达循环标志)。

有趣的一点是︰ -O2 -fno-omit-frame-pointer是只局限于-Os请检查更新的问题。

如果您的程序受代码 L1 缓存,然后突然优化大小开始支付。

上次我检查时,编译器不足够智能,这在所有情况下找出。

在您的案例中-O3 可能为生成代码足够两个缓存行,但操作系统适合在一个缓存行。

要多少打赌那些对齐 = 参数相关的缓存行大小?

我真的不在乎了︰ 它看不到我的计算机上。并通过-falign-*=16标志,一切都已回正常,一切的行为一致。就我而言,这个问题将被解决。

请输入您的翻译

Why does gcc generate 15-20% faster code if I optimize for size instead of speed?

确认取消