Кэш-строки Flu sh в случае теста Single Shot - PullRequest
2 голосов
/ 20 февраля 2020

Я бы хотел запустить 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
    }
}

Есть ли способ Java кэширования гриппа sh до того, как потребуется одноразовое измерение или рукописный clflush?

1 Ответ

1 голос
/ 27 февраля 2020

Если вы хотите измерить пропуски доступа к кешу, вызов clflu sh напрямую возможен из java, но в итоге вы пишете библиотеку JNI с ASM intrinsi c. Нельзя сказать, что вы, вероятно, не можете сделать это надежным способом, поскольку вам нужно предоставить виртуальный адрес, и G C может переместить ваш буфер в любое время.

Вместо этого я предлагаю вам следующее:

  • Используйте эталонный снимок одного снимка, как и вы
  • Измерение одной операции не было бы хорошей идеей (измерение наносекунд имеет высокую погрешность). Вместо этого создайте миллионов таких идентичных буферов и выполните ту же операцию для миллионов буферов. Каждый раз, когда вы получаете доступ к следующему буферу, которого нет в кэше
  • Вы также можете выполнять некоторые вычисления между итерациями. Например, чтение 32+ мегабайт памяти, чтобы исключить строки кэша из вашего кэша. Но с миллионами буферов он не показывает никакой прибыли

Полученный код:

    @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();
}
}

Как видите, я сам являюсь держателем состояния padd, поэтому чтение ссылок на буфера всегда в разных строках кэша (класс Padder имеет 24 длинных поля). О, и я также сам добавляю буфер, JMH не сделает это за вас.

Я реализовал эту идею, и у меня есть среднее значение 100 нс для простой операции, такой как чтение первого элемента буфера. Чтобы прочитать первый элемент, вам нужно прочитать две строки кэша (ссылка на буфер + первый элемент). Полный код здесь

...