解释GCC编译器优化对性能的不利影响?

时间:2020-10-09 09:42:07

标签: c++ performance gcc compiler-optimization

请注意:该问题与代码质量和改进代码的方法无关,对以下问题的(无效)意义也不运行时差异。 这是关于的内容,以及为什么编译器优化会降低性能。

程序

以下代码计算了m以下的斐波那契素数:

int main() {
  unsigned int m = 500000000u;
  unsigned int i = 0u;
  unsigned int a = 1u; 
  unsigned int b = 1u; 
  unsigned int c = 1u;
  unsigned int count = 0u;

  while (a + b <= m) {
    for (i = 2u; i < a + b; ++i) {
      c = (a + b) % i;

      if (c == 0u) {
        i = a + b;
        // break;
      }
    }
    
    if (c != 0u) {
      count = count + 1u;
    } 
  
    a = a + b;
    b = a - b;
  }

  return count; // Just to "output" (and thus use) count
}

如果使用g++.exe (Rev2, Built by MSYS2 project) 9.2.0进行编译且没有优化(-O0),则生成的二进制文件将在1.9秒内(在我的计算机上)执行。使用-O1-O3分别需要3.3秒和1.7秒。

我试图通过查看汇编代码(godbolt.org)和相应的控制流图(hex-rays.com/products/ida)来理解所生成的二进制文件,但是我的汇编技巧并不足够

其他观察结果

  1. 最里面的break中的显式if使-O1代码再次快速:

    if (c == 0u) {
      i = a + b; // Not actually needed any more
      break;
    }
    
  2. “内联”循环的进度表达式也是如此:

    for (i = 2u; i < a + b; ) { // No ++i any more
      c = (a + b) % i;
    
      if (c == 0u) {
        i = a + b;
        ++i;
      } else {
        ++i;
      }
    }
    

问题

  1. 哪个优化可以解释性能下降?

  2. 是否有可能根据C ++代码来解释是什么触发了优化(即,对GCC内部没有深入的了解)?

  3. 类似地,对于替代方案(附加观察结果)为何明显阻止流氓优化的说法,是否有高层解释?

3 个答案:

答案 0 :(得分:2)

我相信这是由编译器在使用-O1-O2时生成的替换分支的“条件移动”指令(CMOVEcc)造成的。

使用-O0时,语句if (c == 0u)被编译为跳转:

    cmp     DWORD PTR [rbp-16], 0
    jne     .L4

使用-O1-O2

    test    edx, edx
    cmove   ecx, esi

-O3产生跳转时(类似于-O0

    test    edx, edx
    je      .L5

There is a known bug in gcc,其中“使用条件移动而不是进行比较和分支导致速度慢将近2倍

rodrigo 在其评论中建议的那样,使用标志-fno-if-conversion告诉gcc不要用条件移动代替分支,从而防止出现此性能问题。

答案 1 :(得分:2)

我认为问题出在org.jboss.resteasy.resteasy-multipart-provider上,指示编译器执行-fif-conversion而不是CMOV进行比较。并且TEST/JZ在一般情况下并不出色。

我知道,反汇编中有两点受此标志影响:

首先,将第13行中的CMOV编译为:

if (c == 0u) { i = a + b; }

第二,test edx,edx //edx is c cmove ecx,esi //esi is (a + b), ecx is i 被编译为

if (c != 0u) { count = count + 1u; }

妙招!它是将cmp eax,0x1 //eax is c sbb r8d,0xffffffff //r8d is count, but what??? 减去-1但带有进位,并且仅当count小于c时才设置进位,这是无符号的,表示1。因此,如果0为0,它将-1减去eax,然后又减去1:它不变。如果count不为0,则会减去eax,从而使变量递增。

现在,这避免了分支,但是以错过明显的优化为代价,即如果-1,您可以直接跳至下一个c == 0u迭代。这个很简单,甚至可以在while中完成。

答案 2 :(得分:2)

这里发挥的重要作用是循环传输的数据依赖性

查看最内层循环的慢速变体的机器代码。我在这里显示-O2程序集,-O1的优化程度较差,但总体上具有相似的数据依赖性:

.L4:
        xorl    %edx, %edx
        movl    %esi, %eax
        divl    %ecx
        testl   %edx, %edx
        cmove   %esi, %ecx
        addl    $1, %ecx
        cmpl    %ecx, %esi
        ja      .L4

了解%ecx中循环计数器的增量如何取决于上一条指令(cmov),该指令又取决于除法的结果,而该结果又取决于除法的值循环计数器。

实际上,在计算%ecx中的值时存在一连串的数据依赖关系,该值跨越整个循环,并且由于执行循环的时间占主导地位,因此计算该链的时间决定了程序的执行时间。

调整程序以计算除数将显示其执行434044698 div指令。在我的情况下,将程序所占用的机器周期数除以该数字可得出26个周期,这与div指令的延迟时间相加,加上链中其他指令的大约3或4个周期(该链为div-test-cmov-add)。

相反,-O3代码没有这种依赖关系链,因此它成为吞吐量约束而不是 latency-bound :执行时间-O3的变体由计算434044698独立的div指令的时间确定。

最后,为您的问题提供具体答案:

1。哪个优化可以解释性能下降?

作为另一个答案,这是if转换在最初存在控制依赖项的情况下创建循环承载的数据依赖项。当控制依赖项对应于不可预测的分支时,控制依赖项也可能代价高昂,但是在这种情况下,分支易于预测。

2。

是否有可能根据C ++代码来解释是什么触发了优化的(即,对GCC的内部没有深入的了解)?

也许您可以想象将代码转换为的优化

    for (i = 2u; i < a + b; ++i) {
      c = (a + b) % i;
      i = (c != 0) ? i : a + b; 
    }

在CPU上对三进制运算符进行求值的地方,使得i的新值在计算c之前是未知的。

3。同样,对于替代方案(附加观察结果)为何明显阻止流氓优化的观点,是否有高层解释?

在这些变体中,代码不适合进行if转换,因此不会引入有问题的数据依赖性。