【问题标题】:Branch prediction: Does avoiding "else" branch for simple operations makes code faster (Java example)?分支预测:避免简单操作的“else”分支是否会使代码更快(Java 示例)?
【发布时间】:2015-04-03 18:00:00
【问题描述】:

选项1:

  boolean isFirst = true;
  for (CardType cardType : cardTypes) {
    if (!isFirst) {
      descriptionBuilder.append(" or ");
    } else {
      isFirst = false;
    }
    //other code not relevant to this theoretical question
  }

选项2:

boolean isFirst = true;
for (CardType cardType : cardTypes) {
  if (!isFirst) {
    descriptionBuilder.append(" or ");
  } 
  isFirst = false;
  //other code not relevant to this theoretical question
}

我的分析:两段代码语义相同。

第一个代码)我不确定这段代码是有两个分支(就分支预测器而言)还是一个分支。我正在研究http://en.wikipedia.org/wiki/X86_instruction_listings,但无法弄清楚是否存在类似于“如果先前的条件值是假的跳转那里”之类的 X86 指令,以避免两个分支预测(非常糟糕)

第二个代码)最有可能总是执行简单的 MOV(寄存器或元素很可能已经在缓存中),这是相对便宜的(最多几个周期)

所以,我的观点是,除非处理器解码成微码指令可以做一些智能的事情或存在 X86 指令以避免必要的分支预测,否则第二个代码会更快。

我知道这纯粹是理论上的问题,因为在实践中,这个分支可以使应用程序快 0.000000002% 或类似的东西。

我错过了什么吗?

编辑:我添加了一个循环,为相关分支提供更多“权重”

EDIT2:问题是关于分支预测的英特尔架构(奔腾和更新的处理器)。

【问题讨论】:

    标签: java assembly compiler-construction branch-prediction vm-implementation


    【解决方案1】:

    代码具有相同的效果,但不会产生相同的字节码或程序集(可能)。

    这对性能有多大影响,尚不清楚,而且可能微不足道。

    远,远更重要的是代码的清晰性。由于在这样的简单情况下代码更难推理,我看到了更多的错误和性能问题。

    简而言之,对您来说最清晰和最简单的也可能足够快,或者最容易修复。

    【讨论】:

    • 这是关于代码性能的理论问题,我知道每次迭代最多会有几个 CPU 周期的差异,但我想知道哪个更快,而不是哪个更具可读性
    • @MladenAdamovic 从理论上讲,一切皆有可能,这就是为什么我避免给出理论上的答案。 ;)
    • 我添加了 EDIT2:问题是关于用于分支预测的英特尔架构(奔腾和更新的处理器)。所以不是“任何事情”都是可能的。 Java 编译器有一定的工作方式,分支预测器有一定的工作方式。
    • @MladenAdamovic 预测它需要创建的分支数量是相同的。唯一不同的是何时可以将简单的操作转变为条件移动。例如a > b ? a : b 在这种情况下,因为使用了 CMOV 指令,所以没有分支。
    • 为什么是一样的?我正在研究 CMOV 指令,似乎添加了该指令以避免此类示例的两个分支a > b ? a : brcollins.org/p6/opcodes/CMOV.html 我的示例 1)有额外的“else”,这可能比我的示例 2)多一个分支)跨度>
    【解决方案2】:

    使用JMH 给出以下数字,cardTypes 数组大小为 10,整数增量为逻辑(Java 15 / AMD 3950X / Windows 10):

    Benchmark          Mode  Cnt          Score         Error  Units
    Benchmark.option1  thrpt   25  273369417.720 ± 1618952.179  ops/s
    Benchmark.option2  thrpt   25  273415784.192 ±  852618.585  ops/s
    

    “选项 2”的平均性能大约快 0.017% (YMMV)。

    另请参阅:branch predictionmethod dispatchmemory accessthroughput and latencygarbage collection

    【讨论】:

    • 请注意,考虑到误差范围,差异在统计上并不显着。但是,是的,这就是我所期望的;对寄存器进行异或归零至少与无条件的jmp 一样便宜。当然更好的是编译器从 asm 循环中提升/剥离第一次迭代,或者在非第一次迭代工作后跳入其中,因此循环中没有实际的 if 分支。但是,如果编译器足够聪明地做到这一点,那么两者都可能会发生这种情况,所以这个基准测试不会捕获它(并且与它兼容,因为 273... +- 1 个数字在错误范围内)
    【解决方案3】:

    不同的硬件对每条汇编指令有不同的成本,在现代硬件上,由于流水线和缓存的影响,甚至一条指令的成本也难以预测。

    在您的孤立示例中,流水线和缓存的 if 和 if/else 之间的区别并不清楚。如果您运行该代码一次,您就不太可能看到任何差异。在一个紧密的循环中反复运行它,if 本身的性能将由 a) 检查成本和 b) 检查结果的可预测性决定。换句话说,分支预测将成为主导因素,并且不会受到 if 或 if/else 代码块的影响。

    关于分支预测效果的精彩讨论可以在这里阅读Why is it faster to process a sorted array than an unsorted array?(见得分最高的答案)。

    【讨论】:

    • 我在示例中添加了外部 for 循环,以便分支预测可以工作(至少理论上)。在这段代码中,“else”是否为 CPU 带来了额外的分支预测?
    • @MladenAdamovic 快速回答是否定的,else 的添加增加了一个额外的 无条件 跳转(根据定义很容易预测)。有关如何将 if/else 转换为汇编程序的示例,请参见 eventhelix.com/realtimemantra/basics/…
    • @MladenAdamovic 但也请参阅我的其他答案; hotspot 喜欢专门针对这种情况展开 for 循环的第一次迭代。这样,它就避免了为循环的后续迭代完全重复检查和条件分支,从而通过提供更快的解决方案绕过您的问题。
    • 感谢@Chris K,感谢您提供的链接让我注意到“else”是无条件跳转。我现在想知道,对于附近的位置(已经在 L1 缓存中,与寄存器或 L1/L2 之间的 MOV 相比,无条件跳转(“else”)需要多少个周期。但正如您所注意到的,编译器可能会发现这里面什么都没有循环将isFirst改为true,所以它可以执行简单的编译器优化。我没有意识到它与展开循环有关,我认为展开循环主要是为了更好的分支预测和并行性
    • @MladenAdamovic 可预测跳跃的成本将以时钟周期(可能为 1 或更少 - 即比人类感知速度快许多数量级)来衡量。所有血淋淋的细节都可以在以下链接中找到,但请注意……阅读量很大:intel.com/content/www/us/en/architecture-and-technology/…
    【解决方案4】:

    假设您的代码 sn-p 是 for 循环中的 if 块。 Hotspot 具有展开 for 循环的能力,这包括进行常见的“是循环的第一次迭代”检查并将其内联到循环之外。从而避免在循环的每次迭代中重新检查条件的成本。从而避免关注 if 或 if/else 哪个更快。

    Oracle 记录了这种行为 here

    【讨论】:

      【解决方案5】:

      两个代码具有相同的语义。

      否两个代码不同,

      如果您的条件 if (!isFirst) 不匹配,则第一个代码应用程序 isFirst = false; 将标志设置为 false。

      第二个代码每次将您的标志更改为false 即使条件满足与否。

      【讨论】:

      • 但标志必须是 false 才能满足条件。所以在这两种情况下,标志在 sn-p 的末尾都有值false
      【解决方案6】:

      if/else 构造中有两个分支:顶部的条件分支,以及 if 部分末尾的 else 部分周围的分支。 else 部分中没有分支,至少在任何实现能力中等的编译器中都没有。

      您必须平衡始终执行isFirst = false; 行的成本。

      在您提到的特定情况下,与方法调用的成本相比,它不太可能产生丝毫差异。

      【讨论】:

      • 我不明白“在 else 部分没有分支,至少在任何甚至中等能力实现的编译器中都没有。”。你能解释一下为什么吗? (汇编代码示例就可以了)。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2020-08-11
      • 1970-01-01
      • 2018-11-30
      • 2018-07-02
      • 2015-12-24
      • 2015-05-11
      • 2013-11-14
      相关资源
      最近更新 更多