Рубрики
Без рубрики

Микробенчмаркирование с Java

Узнайте больше о JMH, Java Microbenchmark Harness.

Автор оригинала: baeldung.

1. Введение

Эта быстрая статья посвящена JMH (Harness Java Microbenchmark). Во-первых, мы знакомимся с API и узнаем его основы. Тогда мы хотели бы видеть несколько лучших практик, которые мы должны рассмотреть при написании микробенчмарков.

Проще говоря, JMH заботится о таких вещах, как JVM разминки и код-оптимизации пути, что делает бенчмаркинг как можно проще.

2. Начало работы

Чтобы начать работу, мы можем продолжать работать с Java 8 и просто определить зависимости:


    org.openjdk.jmh
    jmh-core
    1.28


    org.openjdk.jmh
    jmh-generator-annprocess
    1.28

Последние версии JMH Основные и Процессор аннотации JMH можно найти в Maven Central.

Далее, создать простой ориентир, используя @Benchmark аннотация (в любом общественном классе):

@Benchmark
public void init() {
    // Do nothing
}

Затем мы добавляем основной класс, который начинает процесс бенчмаркинга:

public class BenchmarkRunner {
    public static void main(String[] args) throws Exception {
        org.openjdk.jmh.Main.main(args);
    }
}

Теперь работает БенчмаркРаннер будет выполнять наши, возможно, несколько бесполезный ориентир. После завершения выполнения представлена сводная таблица:

# Run complete. Total time: 00:06:45
Benchmark      Mode  Cnt Score            Error        Units
BenchMark.init thrpt 200 3099210741.962 ± 17510507.589 ops/s

3. Типы контрольных показателей

JMH поддерживает некоторые возможные ориентиры: Пропускная способность, Среднее время, Пример Времени , и СинглШотТайм . Они могут быть настроены с помощью @BenchmarkMode аннотация:

@Benchmark
@BenchmarkMode(Mode.AverageTime)
public void init() {
    // Do nothing
}

Полученная таблица будет иметь среднюю метрику времени (вместо пропускной способности):

# Run complete. Total time: 00:00:40
Benchmark Mode Cnt  Score Error Units
BenchMark.init avgt 20 ≈ 10⁻⁹ s/op

4. Настройка разминки и исполнения

Используя @Fork аннотация, мы можем настроить, как происходит выполнение эталона: значение параметр контролирует, сколько раз будет выполнен эталон, и разминка параметр контролирует, сколько раз эталон высохнет до сбора результатов, например:

@Benchmark
@Fork(value = 1, warmups = 2)
@BenchmarkMode(Mode.Throughput)
public void init() {
    // Do nothing
}

Это поручает JMH запустить две вилки разминки и отказаться от результатов, прежде чем перейти на бенчмаркинг в режиме реального времени.

Кроме того, @Warmup аннотация может быть использована для управления числом итераций разминки. Например, @Warmup (итерации) говорит JMH, что пять итераций разминки будет достаточно, в отличие от по умолчанию 20.

5. Государство

Давайте теперь рассмотрим, как менее тривиальная и более индикативная задача бенчмаркинга алгоритма хэширования может быть выполнена с помощью Государственные . Предположим, что мы решили добавить дополнительную защиту от словарных атак на базу данных паролей, хэшив пароль несколько сотен раз.

Мы можем исследовать влияние производительности с помощью Государственные объект:

@State(Scope.Benchmark)
public class ExecutionPlan {

    @Param({ "100", "200", "300", "500", "1000" })
    public int iterations;

    public Hasher murmur3;

    public String password = "4v3rys3kur3p455w0rd";

    @Setup(Level.Invocation)
    public void setUp() {
        murmur3 = Hashing.murmur3_128().newHasher();
    }
}

Наш метод эталона будет выглядеть так:

@Fork(value = 1, warmups = 1)
@Benchmark
@BenchmarkMode(Mode.Throughput)
public void benchMurmur3_128(ExecutionPlan plan) {

    for (int i = plan.iterations; i > 0; i--) {
        plan.murmur3.putString(plan.password, Charset.defaultCharset());
    }

    plan.murmur3.hash();
}

Здесь поле итерации будут заселены соответствующими значениями от @Param аннотация JMH, когда она передается методу бенчмарка. @Setup аннотированный метод вызывается перед каждым вызовом эталона и создает новый Ашер обеспечение изоляции.

Когда выполнение будет завершено, мы получим результат, аналогичный приведению ниже:

# Run complete. Total time: 00:06:47

Benchmark                   (iterations)   Mode  Cnt      Score      Error  Units
BenchMark.benchMurmur3_128           100  thrpt   20  92463.622 ± 1672.227  ops/s
BenchMark.benchMurmur3_128           200  thrpt   20  39737.532 ± 5294.200  ops/s
BenchMark.benchMurmur3_128           300  thrpt   20  30381.144 ±  614.500  ops/s
BenchMark.benchMurmur3_128           500  thrpt   20  18315.211 ±  222.534  ops/s
BenchMark.benchMurmur3_128          1000  thrpt   20   8960.008 ±  658.524  ops/s

6. Ликвидация мертвого кода

При запуске микробенчмарков очень важно знать об оптимизации . В противном случае они могут повлиять на результаты бенчмарка весьма вводящим в заблуждение образом.

Чтобы сделать ситуацию более конкретной, рассмотрим пример:

@Benchmark
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@BenchmarkMode(Mode.AverageTime)
public void doNothing() {
}

@Benchmark
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@BenchmarkMode(Mode.AverageTime)
public void objectCreation() {
    new Object();
}

Мы ожидаем, что распределение объектов стоит дороже, чем ничего не делать вообще. Однако, если мы забудем тесты:

Benchmark                 Mode  Cnt  Score   Error  Units
BenchMark.doNothing       avgt   40  0.609 ± 0.006  ns/op
BenchMark.objectCreation  avgt   40  0.613 ± 0.007  ns/op

Видимо найти место в TLAB , создание и инициализация объекта практически бесплатна! Просто глядя на эти цифры, мы должны знать, что что-то не совсем добавить здесь.

Здесь мы жертва ликвидации мертвого кода в . Компиляторы очень хорошо оптимизируют избыточный код. На самом деле, это именно то, что компилятор JIT сделал здесь.

Чтобы предотвратить эту оптимизацию, мы должны как-то обмануть компилятор и заставить его думать, что код используется каким-то другим компонентом. Одним из способов достижения этой цели является просто вернуть созданный объект:

@Benchmark
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@BenchmarkMode(Mode.AverageTime)
public Object pillarsOfCreation() {
    return new Object();
}

Кроме того, мы можем позволить Блэкхол потреблять его:

@Benchmark
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@BenchmarkMode(Mode.AverageTime)
public void blackHole(Blackhole blackhole) {
    blackhole.consume(new Object());
}

После Блэкхол потреблять объект — это способ убедить компилятор JIT не применять оптимизацию исключения мертвого кода . Во всяком случае, если мы забудем эти тесты снова, цифры будут иметь больше смысла:

Benchmark                    Mode  Cnt  Score   Error  Units
BenchMark.blackHole          avgt   20  4.126 ± 0.173  ns/op
BenchMark.doNothing          avgt   20  0.639 ± 0.012  ns/op
BenchMark.objectCreation     avgt   20  0.635 ± 0.011  ns/op
BenchMark.pillarsOfCreation  avgt   20  4.061 ± 0.037  ns/op

7. Постоянное складывание

Рассмотрим еще один пример:

@Benchmark
public double foldedLog() {
    int x = 8;

    return Math.log(x);
}

Расчеты, основанные на константах, могут возвращать точно такой же выход, независимо от количества выполнений. Таким образом, существует довольно хороший шанс, что компилятор JIT заменит вызов функции logarithm своим результатом:

@Benchmark
public double foldedLog() {
    return 2.0794415416798357;
}

Эта форма частичной оценки называется постоянным складным . В этом случае постоянная складывание полностью избегает Математика.log вызова, который был весь смысл эталона.

Чтобы предотвратить постоянное складывание, мы можем инкапсулировать постоянное состояние внутри объекта состояния:

@State(Scope.Benchmark)
public static class Log {
    public int x = 8;
}

@Benchmark
public double log(Log input) {
     return Math.log(input.x);
}

Если мы забудем эти тесты друг против друга:

Benchmark             Mode  Cnt          Score          Error  Units
BenchMark.foldedLog  thrpt   20  449313097.433 ± 11850214.900  ops/s
BenchMark.log        thrpt   20   35317997.064 ±   604370.461  ops/s

По-видимому, журнал бенчмарк делает серьезную работу по сравнению с сложенныйLog , что является разумным.

8. Заключение

В этом учебнике основное внимание было сосредоточено на микро-бенчмаркинговой упряжке Java.

Как всегда, примеры кода можно найти на GitHub .