在使用Single Shot基准测试时刷新缓存行

时间:2020-02-20 10:58:48

标签: java jvm jmh

我想运行一个SingleShot JMH基准测试,将与正在处理的内存相关的所有缓存层次结构可靠地清除。

该基准大致如下:

@State(Scope.Benchmark)
public class MyBnchmrk {
    public byte buffer[];

    @Setup(Level.Trial)
    public void generateSampleData() throws IOException {
        // writes to buffer ...
    }

    @Setup(Level.Invocation)
    public void flushCaches() {
         //Perfectly I'd like to invoke here something like
         //_mm_clflushopt() intrinsic as in GCC/clang for each line of the buffer
    }

    @Benchmark
    @BenchmarkMode(Mode.SingleShotTime)
    public void benchmarkMemoryBoundCode() {
        //the benchmark
    }
}

在需要单次测量或手写clflush之前是否存在Java刷新缓存的方法?

1 个答案:

答案 0 :(得分:1)

如果要测量高速缓存未命中的访问,可以从Java直接调用clflush,但是最终使用ASM内部函数编写了JNI库。更不用说,您可能无法以可靠的方式执行此操作,因为您需要提供虚拟地址,GC可能会随时移动缓冲区。

相反,我向您提供:

  • 像以前一样使用单个快照基准测试
  • 测量单个操作不是一个好主意(测量纳秒具有较高的误差)。而是创建百万个此类相同的缓冲区,并对百万个缓冲区执行相同的操作。每次访问下一个不在缓存中的缓冲区
  • 您还可以在迭代之间运行一些计算。例如,读取32 MB以上的内存,以便从缓存中逐出缓存行。但是有数百万个缓冲区,它没有任何利润

结果代码:

    @State(Scope.Benchmark)
@BenchmarkMode(Mode.SingleShotTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Fork(value = 1)
public class BufferBenchmarkLatency {

public static final int BATCH_SIZE = 1000000;

public static final int MY_BUFFER_SIZE = 1024;
public static final int CACHE_LINE_PADDING = 256;

public static class StateHolder extends Padder {
    byte buffer[];

    StateHolder() {
        buffer = new byte[CACHE_LINE_PADDING + MY_BUFFER_SIZE + CACHE_LINE_PADDING];
        Arrays.fill(buffer, (byte) ThreadLocalRandom.current().nextInt());
    }
}

private final StateHolder[] arr = new StateHolder[BATCH_SIZE];
private int index;

@Setup(Level.Trial)
public void setUpTrial() {
    for (int i = 0; i < arr.length; i++) {
        arr[i] = new StateHolder();
    }
    ArrayUtil.shuffle(arr)
}

@Setup(Level.Iteration)
public void prepareForIteration(Blackhole blackhole) {
    index = 0;
    blackhole.consume(CacheUtil.evictCacheLines());
    System.gc();
    System.gc();
}

@Benchmark
public long read() {
    byte[] buffer = arr[index].buffer;
    return buffer[0];
}

@TearDown(Level.Invocation)
public void move() {
    index++;
}

public static void main(String[] args) throws RunnerException {
    Options opt = new OptionsBuilder()
            .include(BufferBenchmarkLatency.class.getSimpleName())
            .measurementBatchSize(BATCH_SIZE)
            .warmupBatchSize(BATCH_SIZE)
            .measurementIterations(10)
            .warmupIterations(10)
            .build();
    new Runner(opt).run();
}
}

如您所见,我填充了状态持有者本身,因此读取缓冲区引用始终位于不同的缓存行中(Padder类具有24个长字段)。哦,我也填充缓冲区本身,JMH不会为您这样做。

我已经实现了这个想法,对于简单的操作(例如读取缓冲区的第一个元素),我的平均结果为100 ns。要读取第一个元素,您需要读取两个缓存行(缓冲区引用+第一个元素)。完整的代码是here