Как разработчики программного обеспечения, мы обычно пишем несколько тестовых программ для сравнения производительности различных алгоритмов и различных инструментов. Наиболее распространенной практикой является написание основного метода для создания моделируемого сценария параллельного тестирования.
Если вы внимательные друзья, то могли обнаружить, что погрешность каждого результата теста очень велика, а иногда результаты теста даже противоречат действительности. Конечно, этого нельзя исключать из-за программных и аппаратных факторов окружающей среды, но это более вероятно, потому что используемый метод тестирования имеет свои проблемы.
Например, в виртуальной машине вызываются разные методы сравнения производительности, которые могут влиять друг на друга и не иметь процесса прогрева.
В этой статье рекомендуется инструмент JMH (Java Microbenchmark Harness), поставляемый с JDK9 и более поздними версиями, который можно использовать для сравнительного анализа программного обеспечения.
О JMH
JMH — это набор инструментов для микротестов кода, в основном основанных на тестах на уровне методов с точностью до наносекунд.
Что такое микро-бенчмарк? Проще говоря, это бенчмарк, основанный на уровне метода, а точность может достигать уровня микросекунд. Когда вы найдете метод горячих точек и хотите дополнительно оптимизировать его производительность, вы можете использовать JMH для количественного анализа результатов оптимизации.
Этот инструмент был написан автором внутренней реализации Oracle JIT. Мы знаем, что JIT (Java Just-In-Time Compiler) — это место, где используются все эффективные средства и методы оптимизации JVM. Как вы понимаете, разработчики лучше, чем кто-либо, знают о влиянии JVM и JIT на бенчмаркинг.
Поэтому этот инструмент достоин нашего доверия и использования на практике. А еще он очень удобен в использовании.
сцены, которые будут использоваться
JMH может не только помочь нам протестировать производительность некоторых распространенных классов, таких как сравнение производительности StringBuffer и StringBuilder, сравнение производительности разных алгоритмов с разным объемом данных и т. д., но и помочь нам количественно проанализировать найденные горячие коды. в системе.
JMH обычно используется в следующих сценариях приложений:
- Проверьте время, необходимое для стабильной работы метода, и корреляцию между временем выполнения и размером проблемы;
- Сравните пропускную способность различных реализаций интерфейса в заданных условиях.
- Посмотрите, какой процент запросов выполнен за какое время
Пример использования
импорт зависимостей
Если вы используете JDK9 или более позднюю версию, этот инструмент уже включен в JDK и может использоваться напрямую. Если вы используете другие версии, вы можете напрямую импортировать следующие зависимости через maven:
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>1.27</version>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>1.27</version>
</dependency>
Среди них 1.27 — последняя версия, которую можно обновить или понизить в соответствии с реальными потребностями.
Прецедент
Давайте возьмем сравнение тестов производительности StringBuffer и StringBuilder в качестве примера для сравнительного анализа.
//使用模式 默认是Mode.Throughput
@BenchmarkMode(Mode.AverageTime)
// 配置预热次数,默认是每次运行1秒,运行10次,这里设置为3次
@Warmup(iterations = 3, time = 1)
// 本例是一次运行4秒,总共运行3次,在性能对比时候,采用默认1秒即可
@Measurement(iterations = 3, time = 4)
// 配置同时起多少个线程执行
@Threads(1)
//代表启动多个单独的进程分别测试每个方法,这里指定为每个方法启动一个进程
@Fork(1)
// 定义类实例的生命周期,Scope.Benchmark:所有测试线程共享一个实例,用于测试有状态实例在多线程共享下的性能
@State(value = Scope.Benchmark)
// 统计结果的时间单元
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class JmhTest {
@Param(value = {"10", "50", "100"})
private int length;
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(JmhTest.class.getSimpleName())
.result("result.json")
.resultFormat(ResultFormatType.JSON).build();
new Runner(opt).run();
}
@Benchmark
public void testStringBufferAdd(Blackhole blackhole) {
StringBuffer sb = new StringBuffer();
for (int i = 0; i < length; i++) {
sb.append(i);
}
blackhole.consume(sb.toString());
}
@Benchmark
public void testStringBuilderAdd(Blackhole blackhole) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < length; i++) {
sb.append(i);
}
blackhole.consume(sb.toString());
}
}
При введении концепции выше было упомянуто, что Benchmark — это тест производительности, при использовании которого вам нужно только добавить аннотацию @Benchmark к тестируемому методу. А в тестовом классе JmhTest указывает разминку, поток, размерность теста и другую информацию о тесте.
В основном методе объект конфигурации теста Options создается с помощью OptionsBuilder, и для запуска теста передается Runner. Результат теста указывается здесь в формате json, а результат сохраняется в файле result.json.
выполнить тест
При выполнении основного метода консоль сначала выводит следующую информацию:
# JMH version: 1.27
# VM version: JDK 1.8.0_271, Java HotSpot(TM) 64-Bit Server VM, 25.271-b09
# VM invoker: /Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/bin/java
# VM options: -javaagent:/Applications/IntelliJ IDEA.app/Contents/lib/idea_rt.jar=56800:/Applications/IntelliJ IDEA.app/Contents/bin -Dfile.encoding=UTF-8
# JMH blackhole mode: full blackhole + dont-inline hint
# Warmup: 3 iterations, 1 s each
# Measurement: 3 iterations, 4 s each
# Timeout: 10 min per iteration
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Average time, time/op
# Benchmark: com.choupangxia.strings.JmhTest.testStringBufferAdd
# Parameters: (length = 10)
Эта информация в основном используется для отображения основной информации о тесте, включая jdk, JVM, конфигурацию прогрева, раунд выполнения, время выполнения, поток выполнения и статистическую единицу теста.
# Warmup Iteration 1: 76.124 ns/op
# Warmup Iteration 2: 77.703 ns/op
# Warmup Iteration 3: 249.515 ns/op
Это предварительная обработка для тестируемого метода, и эта часть не будет зачислена в результаты теста. Прогрев в основном позволяет JVM выполнить достаточную оптимизацию тестируемого кода, например оптимизацию JIT-компилятора.
Iteration 1: 921.191 ns/op
Iteration 2: 897.729 ns/op
Iteration 3: 890.245 ns/op
Result "com.choupangxia.strings.JmhTest.testStringBuilderAdd":
903.055 ±(99.9%) 294.557 ns/op [Average]
(min, avg, max) = (890.245, 903.055, 921.191), stdev = 16.146
CI (99.9%): [608.498, 1197.612] (assumes normal distribution)
Отобразите скорость выполнения каждой итерации (всего 3 раза) и, наконец, сделайте статистику. Вот тест с длиной 100, выполненный в методе testStringBuilderAdd Через три элемента (min, avg, max) можно увидеть значения минимального времени, среднего времени и максимального времени в нс. stdev показывает время расхождения.
Обычно мы просто смотрим на конечный результат:
Benchmark (length) Mode Cnt Score Error Units
JmhTest.testStringBufferAdd 10 avgt 3 92.599 ± 105.019 ns/op
JmhTest.testStringBufferAdd 50 avgt 3 582.974 ± 580.536 ns/op
JmhTest.testStringBufferAdd 100 avgt 3 1131.460 ± 1109.380 ns/op
JmhTest.testStringBuilderAdd 10 avgt 3 76.072 ± 2.824 ns/op
JmhTest.testStringBuilderAdd 50 avgt 3 450.325 ± 14.271 ns/op
JmhTest.testStringBuilderAdd 100 avgt 3 903.055 ± 294.557 ns/op
Мы можем быть удивлены, увидев приведенные выше результаты, мы знаем, что StringBuffer имеет немного более низкую производительность, чем StringBuilder, но оказывается, что разница между ними не очень большая. Это связано с тем, что JIT-компилятор выполняет оптимизацию, например, когда JVM обнаруживает, что StringBuffer не выходит во время теста, поэтому выполняет операцию устранения блокировки.
Общие примечания
Ниже описаны часто используемые аннотации в JHM, чтобы вы могли использовать их более точно.
@BenchmarkMode
Настройте параметр Mode, который воздействует на класс или метод. Его атрибут value представляет собой массив Mode, который может поддерживать несколько режимов одновременно, например: @BenchmarkMode({Mode.SampleTime, Mode.AverageTime}) или его можно установить в Mode.All, то есть сделать все один раз.
org.openjdk.jmh.annotations.Mode — это класс перечисления, и соответствующий исходный код выглядит следующим образом:
public enum Mode {
Throughput("thrpt", "Throughput, ops/time"),
AverageTime("avgt", "Average time, time/op"),
SampleTime("sample", "Sampling time"),
SingleShotTime("ss", "Single shot invocation time"),
All("all", "All benchmark modes");
// 省略其他内容
}
Размеры измерения или способ его измерения различаются в зависимости от режима. В настоящее время существует четыре режима JMH:
- Пропускная способность: общая пропускная способность, например, «сколько вызовов может быть выполнено за 1 секунду», в операциях/времени;
- Среднее время: среднее время вызова, например, «среднее время, затрачиваемое на каждый вызов, составляет xxx миллисекунд», единицей измерения является время/операция;
- SampleTime: случайная выборка и, наконец, вывод распределения результатов выборки, например «99% вызовов в пределах xxx миллисекунд, 99,99% вызовов в пределах xxx миллисекунд»;
- SingleShotTime: все вышеперечисленные режимы используются по умолчанию, одна итерация составляет 1 с, только SingleShotTime запускается только один раз. Количество прогревов часто одновременно устанавливается равным 0, что используется для проверки производительности при холодном запуске;
- Все: Все вышеперечисленные режимы выполняются один раз;
@Warmup
Операция прогрева выполняется перед выполнением @Benchmark, чтобы обеспечить точность теста, который можно использовать для классов или методов. По умолчанию запускается 10 раз каждую 1 секунду.
где @Warmup имеет следующие свойства:
- итерации: количество предварительных прогревов. Итерация — это наименьшая единица измерения, которую JMH может протестировать. В большинстве режимов одна итерация соответствует одной секунде. JMH будет непрерывно вызывать метод, требующий бенчмаркинга, в течение этой секунды, а затем отбирать его в соответствии с режимом. , рассчитать пропускную способность, рассчитать среднее время выполнения и т. д.
- время: время каждой разминки;
- timeUnit: единица времени, по умолчанию секунда;
- batchSize: размер партии, для каждой операции вызывается несколько методов;
JIT будет компилировать горячий код в машинный код во время выполнения и выполнять различные оптимизации для повышения эффективности выполнения. Основная цель предварительного прогрева — заставить работать JIT-механизм JVM и приблизить результат к реальному эффекту.
@State
Аннотация класса, тестовый класс JMH должен использовать аннотацию @State, в противном случае будет выведено сообщение о том, что он не может быть запущен.
Состояние определяет жизненный цикл (область) экземпляра класса, который аналогичен области действия Spring Bean. Поскольку многие эталонные тесты потребуют, чтобы некоторые классы представляли состояние, JMH будет создавать экземпляры и совместно использовать операции в соответствии с областью действия.
@State может использоваться путем наследования.Если родительский класс определяет эту аннотацию, подклассу не нужно ее определять.
Поскольку JMH позволяет нескольким потокам выполнять тесты одновременно, различные параметры имеют следующие значения:
- Scope.Thread: состояние по умолчанию, которое является эксклюзивным для каждого потока, и каждому тестовому потоку назначается экземпляр;
- Scope.Benchmark: это состояние совместно используется всеми потоками, и все тестовые потоки совместно используют экземпляр для проверки производительности экземпляров с отслеживанием состояния при совместном использовании нескольких потоков;
- Scope.Group: это состояние разделяют все потоки в одной группе.
@OutputTimeUnit
Единица времени, используемая для результатов тестовой статистики, которую можно использовать для аннотаций классов или методов, используя стандартную единицу времени в java.util.concurrent.TimeUnit.
@Measurement
На самом деле метрики — это некоторые базовые параметры теста, которые необходимо настроить для фактического вызова метода, который можно использовать в классах или методах. Элементы и функции свойств конфигурации аналогичны @Warmup.
Как правило, более тяжелые программы могут выполнять большое количество тестов и запускать их на сервере. При сравнении производительности время по умолчанию составляет 1 секунду.Если вы используете jvisualvm для мониторинга производительности, вы можете указать более длительное время для запуска.
@Threads
Сколько потоков выполняется одновременно в каждом процессе, которые можно использовать в классах или методах. Значение по умолчанию — Runtime.getRuntime(). AvailableProcessors(), которое выбирается в зависимости от конкретной ситуации, обычно значение ЦП умножается на 2.
@Fork
Представляет запуск нескольких отдельных процессов для тестирования каждого метода по отдельности, которые можно использовать в классах или методах. Если номер вилки равен 2, JMH разветвит два процесса для тестирования.
JVM «печально известна» использованием оптимизации на основе профиля, которая, как известно, недружелюбна к микротестам, поскольку профили из разных методов тестирования смешиваются и «вредят друг другу» результаты тестирования друг друга. Использование отдельного процесса для каждого метода @Benchmark решает эту проблему, что также является вариантом по умолчанию для JMH. Будьте осторожны, чтобы не установить его на 0, установка на n запустит n процессов для выполнения теста (кажется, это не имеет особого смысла). параметры fork также можно установить с помощью аннотаций метода и параметров запуска.
@Param
Аннотации на уровне атрибутов, которые задают различные условия параметра, особенно подходят для тестирования производительности функции в случае ввода разных параметров. Их можно использовать только в полях. Чтобы использовать эту аннотацию, аннотацию @State необходимо определенный.
Аннотация @Param получает массив String, который преобразуется в соответствующий тип данных перед выполнением метода @Setup. Между членами нескольких аннотаций @Param существует отношение продукта.Например, если есть два поля, аннотированные с помощью @Param, первое поле имеет 5 значений, а второе поле имеет 2 значения, тогда каждый метод тестирования будет выполняться 5 * 2 =10 раз.
@Benchmark
Аннотация метода, указывающая, что метод является объектом, который необходимо протестировать, и его использование аналогично @Test в JUnit.
@Setup
Аннотация метода, функция этой аннотации заключается в том, что нам нужно выполнить некоторую подготовительную работу перед тестированием, например инициализацию некоторых данных.
@TearDown
Аннотации методов, в отличие от @Setup, будут выполняться после выполнения всех эталонных тестов, таких как закрытие пулов потоков, подключение к базе данных и т. д., которые в основном используются для повторного использования ресурсов.
Threads
Сколько потоков использует каждый разветвленный процесс для выполнения тестовых методов, значение по умолчанию — Runtime.getRuntime(). AvailableProcessors().
@Group
В аннотации метода вы можете определить несколько тестов как одну и ту же группу, и они будут выполняться одновременно, например, для имитации производительности несоответствия скорости чтения и записи производитель-потребитель.
@Level
Используется для управления временем вызова @Setup и @TearDown, по умолчанию используется Level.Trial.
- Испытание: до и после каждого метода тестирования;
- Итерация: до и после каждой итерации каждого метода тестирования;
- Вызов: до и после вызова каждого метода тестирования используйте его с осторожностью и обращайте внимание на комментарии javadoc;
Примечания JMH
Устранение мертвого кода
Современные компиляторы достаточно умны, чтобы дедуктивно анализировать код, определять, какой код бесполезен, а затем удалять его Это фатальное поведение для микробенчмаркинга, оно может помешать вам точно протестировать производительность вашего метода.
JMH сама разобралась с этой ситуацией, помните: 1. Никогда не пишите метод void 2. Возвращайте результат вычисления в конце метода. Иногда, если вам нужно вернуть более одного результата, вы можете самостоятельно объединить результаты вычислений или использовать объект BlackHole, предоставленный JMH:
/*
* This demonstrates Option A:
*
* Merge multiple results into one and return it.
* This is OK when is computation is relatively heavyweight, and merging
* the results does not offset the results much.
*/
@Benchmark
public double measureRight_1() {
return Math.log(x1) + Math.log(x2);
}
/*
* This demonstrates Option B:
*
* Use explicit Blackhole objects, and sink the values there.
* (Background: Blackhole is just another @State object, bundled with JMH).
*/
@Benchmark
public void measureRight_2(Blackhole bh) {
bh.consume(Math.log(x1));
bh.consume(Math.log(x2));
}
Другим примером является следующий код:
@Benchmark
public void testStringAdd(Blackhole blackhole) {
String a = "";
for (int i = 0; i < length; i++) {
a += i;
}
}
JVM может подумать, что переменная a никогда не используется, и оптимизировать для удаления всего внутреннего кода метода, что повлияет на результаты теста.
JMH предлагает два способа избежать этой проблемы: один — вернуть переменную в качестве возвращаемого значения метода, а другой — избежать устранения JIT-оптимизации за счет использования Blackhole.
Постоянное складывание
Сворачивание констант — это современная стратегия оптимизации компилятора, например, i = 320 * 200 * 32, большинство современных компиляторов на самом деле не генерируют две инструкции умножения и не сохраняют результат, вместо этого они распознают оператор, и значение вычисляется во время компиляции ( я = 2 048 000).
В микробенчмарках, если ваш ввод вычислений предсказуем, а не является переменной экземпляра @State, он, вероятно, будет оптимизирован JIT. В связи с этим JMH предлагает: 1. Всегда считывайте входные данные метода из экземпляра @State 2. Возвращайте результат вычислений 3. Или рассмотрите возможность использования объекта BlackHole;
Смотрите официальный пример ниже:
@State(Scope.Thread)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class JMHSample_10_ConstantFold {
private double x = Math.PI;
private final double wrongX = Math.PI;
@Benchmark
public double baseline() {
// simply return the value, this is a baseline
return Math.PI;
}
@Benchmark
public double measureWrong_1() {
// This is wrong: the source is predictable, and computation is foldable.
return Math.log(Math.PI);
}
@Benchmark
public double measureWrong_2() {
// This is wrong: the source is predictable, and computation is foldable.
return Math.log(wrongX);
}
@Benchmark
public double measureRight() {
// This is correct: the source is not predictable.
return Math.log(x);
}
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(JMHSample_10_ConstantFold.class.getSimpleName())
.warmupIterations(5)
.measurementIterations(5)
.forks(1)
.build();
new Runner(opt).run();
}
}
Размотка петли
Развертывание цикла чаще всего используется для уменьшения накладных расходов на цикл и обеспечения параллелизма на уровне инструкций для процессоров с несколькими функциональными модулями. Это также полезно для планирования конвейера инструкций. Например:
for (i = 1; i <= 60; i++)
a[i] = a[i] * b + c;
Может быть расширен в:
for (i = 1; i <= 60; i+=3){
a[i] = a[i] * b + c;
a[i+1] = a[i+1] * b + c;
a[i+2] = a[i+2] * b + c;
}
Поскольку компилятор может зациклить ваш код, JMH рекомендует не писать никаких циклов в ваших методах тестирования. Если вам нужно выполнить циклические вычисления, вы можете комбинировать @BenchmarkMode(Mode.SingleShotTime) и @Measurement(batchSize = N) для достижения того же эффекта. Обратитесь к следующему примеру:
/*
* Suppose we want to measure how much it takes to sum two integers:
*/
int x = 1;
int y = 2;
/*
* This is what you do with JMH.
*/
@Benchmark
@OperationsPerInvocation(100)
public int measureRight() {
return (x + y);
}
Визуализация JMH
В основном методе примера указывается выходной файл result.json для формирования результатов тестирования, а содержимое сохраняется в формате json.
Контент в формате json может отображаться визуально в виде диаграмм на других сайтах.
Соответствующий веб-сайт, JMH Visual Chart (глубокий ОО VE.com/meet-visual-…Визуализатор (встреча.больше, чем.IO/).
Эффект отображения следующий:
Генерировать выполнение пакета jar
Для крупномасштабных тестов это обычно выполняется на сервере Linux. JMH официально предоставляет способ создания пакета jar для выполнения.Добавьте следующие плагины в maven:
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>2.4.1</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<finalName>jmh-demo</finalName>
<transformers>
<transformer
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>org.openjdk.jmh.Main</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
Выполните команду maven, чтобы сгенерировать исполняемый пакет jar, и выполните:
mvn clean package
java -jar target/jmh-demo.jar JmhTest
Суммировать
Статья охватывает практически все аспекты очков знаний JMH, если вы еще не применяли ее на практике, поторопитесь применить, и ваш профессиональный уровень немного повысится. Конечно, его также можно хранить на случай, если он вам не понадобится время от времени.
Справочная статья:
woo woo woo.cn blog on.com/Commander Vin/Afraid/1…
blog.CSDN.net/Ван Сюэ класс 0…
блог woo woo woo.cn на.com/think-liu/afraid…
Программа Новые Горизонты
публика "Программа Новые Горизонты», платформа, которая позволяет вам одновременно улучшать свою мягкую силу и жесткую технологию, предоставляя массивные данные