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 .