如何理解棘手的提速

How to understand the tricky speed up

本文关键字:何理解      更新时间:2023-10-16

抱歉可能是太抽象的问题,但对我来说这是相当实用的+可能是一些专家有类似的经验并且可以解释它。

我有一个大代码,大约 10000 行大小。

我注意到,如果我在某个地方放

if ( expression ) continue;

其中表达式总是 false(用代码和 cout 的逻辑仔细检查),但依赖于未知参数(因此编译器不能在编译过程中简单地删除这一行),程序的速度提高了 25%(计算结果相同)。如果我测量循环本身的速度,则加速系数大于 3。

为什么会发生这种情况,在没有这些技巧的情况下,有什么可能的方法可以使用这种加速可能性?

附言我使用 gcc 4.7.3, -O3 优化。


更多信息:

  1. 我尝试了两种不同的表达方式,都是有效的。

  2. 如果我将行更改为:

    if ( expression ) { cout << " HELLO " << endl; continue; };
    

    速度消失了。

  3. 如果我将行更改为:

    expression;
    

    速度消失了。

  4. 围绕该行的代码如下所示:

    for ( int i = a; ;  ) {
      do {
        i += d;
        if ( d*i > d*ilast ) break;
          // small amount of calculations, and conditional calls of continue;
      } while ( expression0 );
      if ( d*i > dir*ilast ) break;
      if ( expression ) continue;
       // very big amount calculations, and conditional calls of continue;
    }
    

    for 循环看起来很奇怪。这是因为我修改了循环以抓住这个瓶颈。最初表达式等于表达式 0,而不是 do-loop,我只有这个继续。

  5. 我尝试使用__builtin_expect来理解分支预测。跟

      // the expression (= false) is supposed to be true by branch prediction.
    if ( __builtin_expect( !!(expression), 1) ) continue; 
    

    速度提升为25%。

      // the expression (= false) is supposed to be false by branch prediction.
    if ( __builtin_expect( !!(expression), 0) ) continue; 
    

    速度消失了。

  6. 如果我使用 -O2 而不是 -O3,效果就消失了。该代码比具有 false 条件的快速 O3 版本略慢 (~3%)。

  7. 与"-O2 -finline-functions -funswitch-loops -fpredictive-commoning -fgcse-after-reload -ftree-vectorize"相同。还有一个选项:"-O2 -finline-functions -funswitch-loops -fpredictive-commoning -fgcse-after-reload -ftree-vectorize -fipa-cp-clone"效果被放大了。使用"线"时速度相同,没有"线"时,代码会慢 75%。

  8. 原因是只遵循条件运算符。所以代码看起来像这样:

    for ( int i = a; ;  ) {
          // small amount of calculations, and conditional calls of continue;
      if ( expression ) continue;
        // calculations1
      if ( expression2 ) {
        // calculations2
      }
       // very big amount calculations, and conditional calls of continue;
    }
    

    表达式 2 的值几乎总是假的。所以我这样改了:

    for ( int i = a; ;  ) {
          // small amount of calculations, and conditional calls of continue;
      // if ( expression ) continue; // don't need this anymore
        // calculations1
      if ( __builtin_expect( !!(expression2), 0 ) ) { // suppose expression2 == false
        // calculations2
      }
       // very big amount calculations, and conditional calls of continue;
    }
    

    并得到了所需的 25% 加速。甚至更多一点。行为不再取决于临界线。

如果有人知道可以解释这种行为而无需猜测的材料,我将很高兴阅读并接受他们的答案

找到了。

原因在于紧随其后的条件运算符。所以代码看起来像这样:

for ( int i = a; ;  ) {
      // small amount of calculations, and conditional calls of continue;
  if ( expression ) continue;
    // calculations1
  if ( expression2 ) {
    // calculations2
  }
   // very big amount calculations, and conditional calls of continue;
}

表达式 2 的值几乎总是假的。所以我这样改了:

for ( int i = a; ;  ) {
      // small amount of calculations, and conditional calls of continue;
  // if ( expression ) continue; // don't need this anymore
    // calculations1
  if ( __builtin_expect( !!(expression2), 0 ) ) { // suppose expression2 == false
    // calculations2
  }
   // very big amount calculations, and conditional calls of continue;
}

并得到了所需的 25% 加速。甚至更多一点。行为不再取决于临界线。


我不知道如何解释它,也找不到足够的分支预测材料。

我想重点是应该跳过计算 2,但编译器不知道这一点,并假设默认情况下表达式 2 == true。同时,它假设在简单的继续检查中

if ( expression ) continue;

表达式 == false,并且在任何情况下都必须很好地跳过计算2。如果我们有更复杂的操作(例如 cout),则假设表达式为真并且技巧不起作用。

如果有人知道可以解释这种行为而无需猜测的材料,我将很高兴阅读并接受他们的答案。

引入那个不可能到达的分支会破坏流图。通常,编译器知道执行流是从循环的顶部直接到出口测试,然后再回到开始。现在,图中有一个额外的节点,流可以在其中离开循环。它现在需要以不同的方式编译循环体,分为两部分。

这几乎总是导致更糟糕的代码。为什么它不在这里,我只能提供一个猜测:你没有使用分析信息进行编译。因此,编译器必须做出假设。特别是,它必须假设分支将在运行时被采用的可能性。

显然,由于它必须做出的假设是不同的,因此生成的代码很可能在速度上有所不同。

不想这么说,但答案将是相当技术性的,更重要的是,非常特定于你的代码。 如此之多,以至于除了你自己之外,可能没有人会花时间去调查你问题的根源。 正如其他人所建议的那样,它很可能取决于分支预测和其他与流水线相关的编译后优化。

如果这是编译器优化问题或编译后 (CPU) 优化,我唯一能建议您缩小范围的建议是再次编译您的代码,使用 -O2-O3 ,但这次添加以下附加选项:-fverbose-asm -S . 将每个输出通过管道传输到两个不同的文件,然后运行类似 sdiff 的内容来比较它们。 您应该会看到很多差异。

不幸的是,如果没有对汇编代码的良好理解,就很难对它进行正面或反面,老实说,Stack Overflow 上没有多少人有耐心(或时间)在这个问题上花费超过几分钟的时间。 如果你不精通汇编(大概是x86),那么我建议找一个同事或朋友来帮助你解析汇编输出。