Бенчмаркинг JMH позволяет избежать оптимизации jvm

Я пытаюсь написать тесты jmh.

Я наткнулся на различные блоги, в которых упоминаются подводные камни в бенчмаркинге jmh. Типичным примером являются

  1. этот код
int sum() {
   int a =7;
   int b = 8;
   return a+b;
}

будет оптимизирован для

int sum() {
return 15;
}
  1. этот код
int sum(int y) {
   int x = new Object();
   return y;
}

будет оптимизирован для

int sum(int y) {
   return y;
}

то есть удаление неиспользуемой инициализации объекта.

Но этот список далеко не исчерпывающий, что может сделать jvm для оптимизации.

Ниже приведена проблема, с которой я столкнулся.

Допустим, есть несколько методов, и вот как выглядит граф вызовов

int methodA(CustomObjectA a) {
   //do something 
   methodB(a);
   //do something
   return returnValueA;
}

int methodB(CustomObjectA a) {
   //do something 
   methodC(a);
   //do something
   return returnValueB;
}

int methodC(CustomObjectA a) {
   //do something
   return returnValueC;
}

И мы попробуем протестировать метод А. Передавая CustomObjectA, созданный в объекте состояния. Но

  1. с точки зрения JVM methodC всегда вызывается с одной и той же ссылкой, не будет ли он оптимизировать methodC для постоянного возврата одного и того же returnValueC?

  2. Почему бы не сделать это?

  3. Как мы можем убедиться, что эта оптимизация не будет выполнена? передавая разные ссылки каждый раз, используя @State(Scope.Thread)?

  4. Есть ли какой-нибудь исчерпывающий список, чтобы объяснить, что вся оптимизация возможна?


person best wishes    schedule 07.04.2021    source источник
comment
в int methodC(CustomObjectA a) почему вы передаете a в качестве аргумента, если вы ничего с ним не делаете?   -  person Eugene    schedule 07.04.2021
comment
я делаю что-то с   -  person best wishes    schedule 07.04.2021


Ответы (1)


Вы говорите, что хотите протестировать methodA, а все остальные методы private, и вот как выглядит цепочка вызовов? Если это так, то JMH здесь не имеет значения — какие оптимизации будут применяться, все равно будут применяться к этому коду. Также совершенно невозможно сказать, какие оптимизации могут произойти в конце, так как их много на JVM, а также зависит от множества других факторов, таких как операционная система, процессор и т. д.; поэтому обширный список просто не может существовать.

Например, в зависимости от того, что вы делаете в этом //do something в каждом методе, этот код может быть опущен или нет. Посмотрите на этот упрощенный пример:

@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Warmup(iterations = 5, time = 5)
@Measurement(iterations = 5, time = 5)
public class Sample {

    private static final int ME = 1;

    public static void main(String[] args) throws Exception {
        Options opt = new OptionsBuilder()
                .include(Sample.class.getSimpleName())
                .build();

        new Runner(opt).run();
    }

    @Benchmark
    public int methodOne(CustomObjectA a) {
        simulateWork();
        return 42;
    }

    @Benchmark
    public int methodTwo(CustomObjectA a, Blackhole bh) {
        bh.consume(simulateWork());
        return 42;
    }

    @State(Scope.Thread)
    public static class CustomObjectA {

    }

    private static double simulateWork() {
        return ME << 1;
    }

}

Разница в том, что в методе methodTwo я использую так называемый Blackhole (читать это для более подробной информации), а в methodOne я этого не делаю. В результате simulateWork исключается из methodOne, как показывают результаты:

Benchmark         Mode  Cnt  Score   Error  Units
Sample.methodOne  avgt   25  1.950 ± 0.078  ns/op
Sample.methodTwo  avgt   25  3.955 ± 0.120  ns/op

С другой стороны, если я даже немного изменю код, чтобы иметь наименьшие возможные побочные эффекты:

@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Warmup(iterations = 5, time = 5)
@Measurement(iterations = 5, time = 5)
public class Sample {

    public static void main(String[] args) throws Exception {
        Options opt = new OptionsBuilder()
                .include(Sample.class.getSimpleName())
                .build();

        new Runner(opt).run();
    }

    @Benchmark
    public int methodOne(CustomObjectA a) {
        simulateWorkWithA(a);
        return 42;
    }

    @Benchmark
    public int methodTwo(CustomObjectA a) {
        return simulateWorkWithA(a) + 42;
    }

    @Benchmark
    public int methodThree(CustomObjectA a, Blackhole bh) {
        bh.consume(simulateWorkWithA(a));
        return 42;
    }

    @State(Scope.Thread)
    public static class CustomObjectA {
        int x = 0;
    }

    private static int simulateWorkWithA(CustomObjectA a) {
        return a.x = a.x + 1;
    }

}

устранение simulateWorkWithA(a) в methodOne не произойдет:

Benchmark           Mode  Cnt  Score   Error  Units
Sample.methodOne    avgt   25  2.267 ± 0.198  ns/op
Sample.methodThree  avgt   25  3.711 ± 0.131  ns/op
Sample.methodTwo    avgt   25  2.325 ± 0.008  ns/op

Обратите внимание, что почти нет разницы между methodOne и methodTwo.

person Eugene    schedule 07.04.2021
comment
Спасибо за упоминание extensive list simply can't exist.. в этом случае вот мои проблемы: 1. скажем, есть статический метод, и мы передаем ему одну и ту же переменную снова и снова, не будет ли он кэшировать значение ответа для одного и того же входного параметра (PropertyUtils.getProperty(Object bean, String имя) если быть точным)? 2. В случае вызова вложенного метода я не могу изменить исходный код, чтобы полностью передать blackhole, верно? 3. Является ли бенчмаркинг единственным способом увидеть, что может происходить во всех оптимизациях? потому что в этом случае становится сложно убедиться, что чьи-то бенчмарки верны. - person best wishes; 08.04.2021
comment
@bestwishes для (1), да, это может быть оптимизировано для этого. И что? Для (2) - да, нельзя. Но посмотрите на второй пример, который я привел, и поймите его: даже если оптимизация будет выполнена, она все равно будет происходить незаметно. Для (3): я не очень понимаю ваш запрос. Общий смысл здесь таков: да, происходит много оптимизаций, выделения могут быть исключены, переменные могут быть встроены и т. д. - это не имеет значения. Если вы хотите измерить methodA - измерьте его таким, какой он есть. - person Eugene; 08.04.2021
comment
yes, it might optimise to that. so what? ››› но на продукте вероятность этого была бы ниже, верно? следовательно, любой тест, о котором я буду сообщать, будет неправильным. it does not matter. If you want to measure methodA - measure it the way it is. ›› но в производственной среде тест не будет верным, так как там все будет более случайным (а не фиксированным набором входных данных. ), и любая оптимизация, выполняемая в тесте, не будет выполняться в производственной среде. - person best wishes; 08.04.2021
comment
@bestwishes, это не было бы ошибкой, это все еще правильно, но не в соответствии с тем, что вы видите в своей постановке. Имейте в виду, что JMH имеет разные режимы, например, только в C1 jit-компиляторе, или также холодный запуск - person Eugene; 08.04.2021