分支预测不起作用吗?

MYK*_*MYK 13 java performance microbenchmark branch-prediction jmh

在参考这个问题时,答案指出未排序的数组花费更多时间,因为它未通过分支预测测试.但如果我们对程序进行微小改动:

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);
    }
}
Run Code Online (Sandbox Code Playgroud)

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

if (data[c] >= 128) 
    sum += data[c];
Run Code Online (Sandbox Code Playgroud)

if (data[c] >= 128) 
    sum = data[c];
Run Code Online (Sandbox Code Playgroud)

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

Mar*_*nik 10

我用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;
  }
}
Run Code Online (Sandbox Code Playgroud)

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

data[c] = (i*=611953);
Run Code Online (Sandbox Code Playgroud)

我的运行时间为132μs.如果我注释掉涉及的行

data[c] = data[c] >= 128? 128 : 127;
Run Code Online (Sandbox Code Playgroud)

时间不会改变.这消除了所有算术考虑,并侧重于分支预测.如果我使用

data[c] = 127;
Run Code Online (Sandbox Code Playgroud)

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

data[c] = 128;
Run Code Online (Sandbox Code Playgroud)

我得到16μs.这是"基本案例",强调不断分支决策之间的区别.

我的结论:这绝对是低级分支预测的结果.

JIT可以扭转循环吗?

我们现在分析你的干预.如果我使用上面代码中提供的公式,但更改

if (data[c] >= 128) sum += data[c];
Run Code Online (Sandbox Code Playgroud)

if (data[c] >= 128) sum = data[c];
Run Code Online (Sandbox Code Playgroud)

然后时间确实从132μs下降到27μs.

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

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

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

我加了这个

data[SIZE-1] = 128;
Run Code Online (Sandbox Code Playgroud)

setup()方法结束,但它没有改变时间.这似乎使"循环逆转"猜想的天真版本无效.

不,这可能是 cmovl

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

cmp edx, 0x80
cmovl eax, ebx
Run Code Online (Sandbox Code Playgroud)

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