【问题标题】:Is branch prediction not working?分支预测不起作用吗?
【发布时间】:2014-02-21 08:00:48
【问题描述】:

关于this 问题,答案指定未排序数组需要更多时间,因为它未通过分支预测测试。但是如果我们对程序做一个小的改动:

import java.util.Arrays;
import java.util.Random;


public class Main{

    public static void main(String[] args) {
        // Generate data
        int arraySize = 32768;
        int data[] = new int[arraySize];

        Random rnd = new Random(0);
        for (int c = 0; c < arraySize; ++c) {
            data[c] = rnd.nextInt() % 256;
        }

        // !!! With this, the next loop runs faster
        Arrays.sort(data);

        // Test
        long start = System.nanoTime();
        long sum = 0;

        for (int i = 0; i < 100000; ++i) {
            // Primary loop
            for (int c = 0; c < arraySize; ++c) {
                if (data[c] >= 128) {
                    sum = data[c];
                }
            }
        }

        System.out.println((System.nanoTime() - start) / 1000000000.0);
        System.out.println("sum = " + sum);
    }
}

这里我已经替换(来自原始问题)

if (data[c] >= 128) 
    sum += data[c];

if (data[c] >= 128) 
    sum = data[c];

未排序的数组给出了大约。同样的结果,我想问为什么分支预测在这种情况下不起作用?

【问题讨论】:

  • 您在比较哪些结果?
  • 我也使用了排序和未排序的数组。在 Core i5-3230M 上大约需要 5 秒,通常未排序的速度更快,但由于随机性,可以忽略...
  • 拜托...这不是您在 JVM 上测量 anything 的方式。 Java 不是 C,它有一个非常复杂的 runtime,它会在某个时间点对代码进行 JIT 编译,这不是初始点——还有无数其他影响测量的问题。跨度>
  • @zapl "Java 不能在执行分支预测的处理器上运行" => 当然可以(或者学究一下:执行 java 代码的 JVM 可以)!
  • @MarkoTopolnik 我可以重现他的描述:使用sum += data[c] 有或没有排序显示性能差异(排序快3 倍)但使用sum = data[c] 有或没有排序显示没有性能差异。跨度>

标签: java performance microbenchmark branch-prediction jmh


【解决方案1】:

我已经使用 jmh 来分析这个。这是我的代码:

@OutputTimeUnit(TimeUnit.MICROSECONDS)
@BenchmarkMode(Mode.AverageTime)
@Warmup(iterations = 2, time = 1)
@Measurement(iterations = 3, time = 1)
@State(Scope.Thread)
@Fork(2)
public class Comparison
{
  static final int SIZE = 1<<15;
  final int[] data = new int[SIZE];

  @Setup
  public void setup() {
    int i = 1;
    for (int c = 0; c < SIZE; ++c) data[c] = (i*=611953);
    for (int c = 0; c < SIZE; ++c) data[c] = data[c] >= 128? 128 : 127;
  }

  @GenerateMicroBenchmark
  public long sum() {
    long sum = 0;
    for (int c = 0; c < SIZE; ++c) if (data[c] >= 128) sum += data[c];
    return sum;
  }
}

请注意,我不使用排序或随机数生成;它们是不必要的并发症。使用上面代码中使用的公式:

data[c] = (i*=611953);

我得到 132 µs 的运行时间。如果我注释掉涉及

的行
data[c] = data[c] >= 128? 128 : 127;

时间完全没有变化。这消除了所有算术考虑并专注于分支预测。如果我使用

data[c] = 127;

我得到 13 µs,如果我使用

data[c] = 128;

我得到 16 µs。这是“基本情况”,强调了不断分支决策之间的区别。

我的结论:这肯定是低级分支预测的效果。

JIT 能否逆转循环?

现在让我们分析您的干预。如果我使用上面代码中给出的公式,但更改

if (data[c] >= 128) sum += data[c];

if (data[c] >= 128) sum = data[c];

那么时间确实从 132 µs 下降到 27 µs。

这是我解释下降的猜测:JIT 编译器可以做的一个优化技巧是反转循环的方向。现在你的代码变成了

for (int c = SIZE-1; c <= 0; --c) if (data[c] >= 128) { sum = data[c]; break; }

循环已短路,达到与原始循环相同的结果所需的最小迭代次数。

我添加了这个

data[SIZE-1] = 128;

setup() 方法的末尾,但它并没有改变时间。这似乎使“循环反转”猜想的幼稚版本无效。

不,可能是cmovl

在分析程序集时,我发现:

cmp edx, 0x80
cmovl eax, ebx

cmovl 是一个条件移动 指令,它将执行发生在then 分支中的赋值效果,但不涉及任何跳转,因此消除了与分支预测失败相关的任何惩罚。这是对实际效果的一个很好的解释。

【讨论】:

  • @assylias 我已经大大改变了我的答案。
  • 名侦探。 CMOVL 肯定会删除分支。智能 JIT!
  • @assylias:智能 JIT,但 only sometimes。 CMOVL 也可以用于+=,我会说 JIT 有时会这样做,而且决策不是最佳的。
  • @maaartinus 确实 - 我记得那个帖子。
  • @maaartinus 在这个问题中由 OP 链接的帖子中暗示了这一点。在那个问题中,评论中的一位受访者说他根本没有观察到差异,在 gcc 上使用了适当的 -O 设置。
猜你喜欢
  • 2019-03-31
  • 2012-02-12
  • 2014-04-25
  • 2014-03-03
  • 2016-07-01
  • 2011-02-01
  • 2015-11-24
  • 2012-07-02
  • 1970-01-01
相关资源
最近更新 更多