【问题标题】:How to understand the tricky speed up如何理解棘手的加速
【发布时间】:2013-10-28 16:12:46
【问题描述】:

对不起,这个问题可能太抽象了,但对我来说这很实用+可能有一些专家有类似的经验,可以解释一下。

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

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

if ( expression ) continue;

其中表达式总是假(用代码和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”效果会被放大。使用“line”的速度是一样的,没有“line”的代码会慢 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% 加速。甚至更多一点。而且行为不再取决于临界线。

如果有人知道材料,可以不用猜测就能解释这种行为,我会很高兴阅读并接受他们的回答。

【问题讨论】:

  • 我怀疑没有SSCCE我们可以告诉你一些事情。
  • 这可能是平台和编译器特定的行为;如果不查看周围的代码,这将是非常难以判断的。 (除非您弄错了,并且实际上有时表达是正确的。)最好将其简化为 SSCCE。 sscce.org
  • 如果没有看到更多代码和您已完成的基准测试的细分(例如,运行时在多次运行中更改前后是否一致?),将很难缩小范围。
  • 听起来像是分支预测的结果。
  • 如果您可以公开您的代码,您可以参与改进 gcc 并提交错误报告。

标签: c++ optimization cpu-speed


【解决方案1】:

找到了。

原因在于下面的条件运算符。所以代码看起来像这样:

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,但编译器不知道这一点,并且默认情况下假设 expression2 == true。 同时假设在简单的继续检查中

if ( expression ) continue;

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

如果有人知道材料,可以不用猜测就能解释这种行为,我会很高兴阅读并接受他们的回答。

【讨论】:

    【解决方案2】:

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

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

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

    【讨论】:

    • 很明显。我已经调查了这些假设。请参阅附加信息,第 5 项。正如预期的那样,只有当编译器认为它在大多数情况下等于 true 时,该行才有帮助。
    • 好吧,看来这是正确的答案:强制该假设会导致完全相同的加速,因此它肯定取决于假设并扩展分支预测。
    • 但这怎么可能呢?编译器创建更多代码部分并开始执行错误的部分。代码应该更慢...
    • 很可能是您在下面需要的变量的意外预取。堆栈布局也不同。
    • 我尝试了不同的变量。包括早期引入的附加变量 == 表达式,我从来没有使用过。另见第 3 项。
    【解决方案3】:

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

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

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2011-01-20
      • 1970-01-01
      • 1970-01-01
      • 2014-07-13
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多