【问题标题】:What techniques to avoid conditional branching do you know?您知道哪些避免条件分支的技术?
【发布时间】:2010-12-09 19:42:46
【问题描述】:

有时,CPU 花费大部分时间的循环经常会出现一些分支预测未命中(错误预测)(概率接近 0.5)。我在非常孤立的线程上看到了一些技术,但从未见过列表。我知道的那些已经修复了条件可以转换为布尔值并且 0/1 以某种方式用于更改的情况。还有其他可以避免的条件分支吗?

例如(伪代码)

loop () {
  if (in[i] < C )
    out[o++] = in[i++]
  ...
}

可以重写,可能会失去一些可读性,如下所示:

loop() {
  out[o] = in[i]  // copy anyway, just don't increment
  inc = in[i] < C  // increment counters? (0 or 1)
  o += inc
  i += inc
}

我还看到,在某些情况下,我现在在某些情况下,在条件条件下将&amp;&amp; 更改为&amp;。我是这个优化级别的新手,但感觉肯定还有更多。

【问题讨论】:

  • 不好的例子。即使无分支代码可以被视为等同于原始代码,那也只是在原始代码一开始就没有任何意义的情况下。
  • 为什么这么多人回答的答案实际上并没有回答这个问题,这超出了我的理解

标签: c optimization assembly


【解决方案1】:

使用 Matt Joiner 的例子:

if (b > a) b = a;

您还可以执行以下操作,而无需深入研究汇编代码:

bool if_else = b > a;
b = a * if_else + b * !if_else;

【讨论】:

  • 您可以将乘法替换为按位与。您所要做的就是将 if_else 预处理为位掩码:unsigned int yes_mask = (unsigned int)(-(int)if_else); unsigned int no_mask = yes_mask ^ 0xffffffff;,然后像这样使用它:b = a &amp; yes_mask | b &amp; no_mask。另一方面,足够先进的处理器可以通过分支来减慢速度,因此乘法速度可能很快,因此只有在多次重复使用掩码时才会更快。
【解决方案2】:

我认为避免分支的最常见方法是利用位并行性来减少代码中出现的总跳转。基本块越长,刷新管道的频率就越低。

正如其他人所提到的,如果您想做的不仅仅是展开循环和提供分支提示,那么您将希望进入汇编。当然,这应该非常谨慎地完成:在大多数情况下,您的典型编译器可以编写比人类更好的程序集。您最好的希望是去除粗糙的边缘,并做出编译器无法推断的假设。

以下是以下 C 代码的示例:

if (b > a) b = a;

在没有任何跳转的汇编中,通过使用位操作(和极端注释):

sub eax, ebx ; = a - b
sbb edx, edx ; = (b > a) ? 0xFFFFFFFF : 0
and edx, eax ; = (b > a) ? a - b : 0
add ebx, edx ; b = (b > a) ? b + (a - b) : b + 0

请注意,虽然汇编爱好者会立即开始使用条件移动,但这仅仅是因为它们易于理解,并且在一条方便的单条指令中提供了更高级别的语言概念。它们不一定更快,在旧处理器上不可用,通过将 C 代码映射到相应的条件移动指令,您只是在完成编译器的工作。

【讨论】:

  • 嗯,你的汇编代码不是假设sub eax, exb没有溢出吗?
【解决方案3】:

您给出的示例的概括是“用数学替换条件评估”;条件分支避免很大程度上归结为这一点。

&amp;&amp; 替换为&amp; 会发生什么,因为&amp;&amp; 是短路的,它本身就构成了条件评估。 &amp; 如果两边都为 0 或 1,并且不是短路,则为您提供相同的逻辑结果。同样适用于|||,除非您不需要确保边被限制为 0 或 1(同样,仅出于逻辑目的,即您仅使用布尔结果)。

【讨论】:

    【解决方案4】:

    在这个级别,事情非常依赖硬件和编译器。您使用的编译器是否足够聪明,可以在没有控制流的情况下编译 lcc 不是。在较旧的或嵌入式指令集上,可能无法在没有控制流的情况下计算

    除了这个类似 Cassandra 的警告之外,很难做出任何有用的一般性陈述。所以这里有一些可能没有帮助的一般性陈述:

    • 现代分支预测硬件非常好。如果你能找到一个真正的程序,其中错误的分支预测会导致超过 1%-2% 的减速,我会感到非常惊讶。

    • 性能计数器或其他告诉您在哪里可以找到分支错误预测的工具是必不可少的。

    • 如果您确实需要改进此类代码,我会考虑跟踪调度和循环展开:

      • 循环展开复制循环体,并为优化器提供更多控制流。

      • 跟踪调度确定最有可能采用哪些路径,除其他技巧外,它还可以调整分支方向,以便分支预测硬件在最常见的路径上更好地工作。使用展开的循环,路径越来越多,因此跟踪调度程序可以使用更多

    • 我对自己在汇编中编写代码持谨慎态度。当下一个带有新分支预测硬件的芯片问世时,您的所有辛勤工作很有可能付诸东流。相反,我会寻找一个反馈导向的优化编译器

    【讨论】:

    • 酷,谢谢!我正在对大型数据集进行 SIMD 压缩、排序和搜索。当概率大约为 0.5 时会有所不同(这就是为什么一开始就有这个问题。)好吧,保存安腾或类似的架构,但这不是我的情况。数据的性质将有很大差异,因为它不是专门用于某种数据集(它可能是随机的、增量的等),因此反馈会有所帮助,但在一定程度上是有帮助的。并且有很多像问题中的示例这样的情况,甚至无需深入组装即可轻松解决。那是我的追求:)
    【解决方案5】:

    当您必须执行多个嵌套测试才能获得答案时,适用于原始问题中演示的技术的扩展。您可以根据所有测试的结果构建一个小的位掩码,并在表格中“查找”答案。

    if (a) {
      if (b) {
        result = q;
      } else {
        result = r;
      }
    } else {
      if (b) {
        result = s;
      } else {
        result = t;
      }
    }
    

    如果 a 和 b 几乎是随机的(例如,来自任意数据),并且这是一个紧密的循环,那么分支预测失败会真正减慢这一速度。可以写成:

    // assuming a and b are bools and thus exactly 0 or 1 ...
    static const table[] = { t, s, r, q };
    unsigned index = (a << 1) | b;
    result = table[index];
    

    您可以将其概括为多个条件句。我已经看到它完成了 4 次。但是,如果嵌套变得那么深,您需要确保测试所有这些确实比仅执行短路评估建议的最小测试要快。

    【讨论】:

      【解决方案6】:

      GCC 已经足够聪明,可以用更简单的指令替换条件。例如,较新的 Intel 处理器提供 cmov(条件移动)。如果可以使用,SSE2 会一次向compare 4 integers 提供一些指令(或 8 个短裤,或 16 个字符)。

      另外计算您可以使用的最小值(请参阅这些magic tricks):

      min(x, y) = x+(((y-x)>>(WORDBITS-1))&(y-x))
      

      但是,请注意以下事项:

      c[i][j] = min(c[i][j], c[i][k] + c[j][k]);   // from Floyd-Warshal algorithm
      

      即使没有隐含跳跃也比

      慢得多
      int tmp = c[i][k] + c[j][k];
      if (tmp < c[i][j])
          c[i][j] = tmp;
      

      我最好的猜测是,在第一个 sn-p 中,您会更频繁地污染缓存,而在第二个中则不会。

      【讨论】:

      • 请注意,cmov 的缺点是,从指令重新排序和并行执行的角度来看,它被视为依赖于其源操作数。对于通常为假的条件,良好预测的条件跳转可能比停滞的cmov 更快。
      【解决方案7】:

      在我看来,如果您要达到这种优化水平,那么可能是时候直接使用汇编语言了。

      本质上,您指望编译器生成特定的汇编模式,以利用 C 中的这种优化。很难准确猜测编译器将生成什么代码,因此您必须在任何时候进行小改动时查看它 - 为什么不直接在汇编中进行并完成它呢?

      【讨论】:

      • 是的。这就是装配标签的原因。如果你有这种优化的汇编技术,如果你能分享(链接也!)将不胜感激!
      • 我不确定我可以分享多少东西——我的程序集主要是在读取端(调试时)或在嵌入式 C 中无法完成的硬件级别的工作(不是优化)系统。突然出现在我脑海中的一件事是 ARM 特定的,并不是什么花招。 ARM 指令有一个字段允许有条件地执行它们,因此它们不必在它们周围跳转,而是有效地成为 NOP,对指令流水线没有影响。
      【解决方案8】:

      大多数处理器提供优于 50% 的分支预测。事实上,如果你在分支预测方面得到 1% 的改进,那么你可能可以发表一篇论文。如果您有兴趣,有大量关于此主题的论文。

      最好不要担心缓存命中和未命中。

      【讨论】:

      • 我发现——至少在某些情况下——分支预测未命中的解决方案通常对缓存性能也更好。这可能是双赢的。
      【解决方案9】:

      除了最热门的热点之外,这种优化水平不太可能对所有热点产生有价值的影响。假设它确实(没有在特定情况下证明它)是一种猜测,优化的第一条规则是不要对猜测采取行动

      【讨论】:

      • 我认为问题中的示例非常真实,远非猜测。事实上,它就在这段代码中。这当然是用于压缩/排序/搜索的紧循环的最里面的组件,所以它绝对是一个热点。它不只是为了踢球而优化 hello-world。谢谢。
      • @aleccolocco:这就是我的意思。选择一个真正的程序,而不是为了提出问题而创建的程序。对其进行一些性能调整,以真正解决它。诸如分支预测之类的问题在其他所有内容都用尽之前不会出现,因此从假设它们真正重要的假设开始并不是基于知道问题实际上是什么。 stackoverflow.com/questions/926266/…
      • ...同时,当您确实遇到这样的热点时,您是对的,它们可以有所作为。 (对不起。对我来说,这是一个热点问题,许多人似乎认为优化在低级别开始和结束,而这只是冰山一角。)
      • @MikeDunlavey 是的,确实。还有更隐蔽的性能损失,例如页面拆分或缓存行拆分。但我已经知道如何处理这些(并且预防措施已经在设计中)。干杯。
      猜你喜欢
      • 2011-02-20
      • 1970-01-01
      • 2010-12-06
      • 2013-07-26
      • 2017-12-31
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多