Микробенчмаркинг с jmh in Kotlin
Захотелось осветить тему для коллег с микробенчмаркингом в kotlin. И сделать небольшой обзор для старта на русском. Статья освящает моменты с запуском и дает небольшой FAQ по известным проблемам.
Jmh - Java Microbenchmark Harness, библиотека для тестирования производительности вашего java кода.
Перед тем, как запустить jmh ваша программа должна быть скомпилена в байт код, но благо для всего этого уже есть плагин. Также будьте готовы к тому, что бенчмаркинг дело не быстрое, в среднем прогон примеров занял 11 минут 53 секунды на i5 7 поколения.
Подключение
Сконфигурим наш build.gradle следующим образом:
plugins {
id 'org.jetbrains.kotlin.jvm' version '1.3.50'
id "me.champeau.gradle.jmh" version "0.4.8"
}
repositories {
mavenCentral()
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib"
jmh 'org.openjdk.jmh:jmh-core:1.20'
jmh 'org.openjdk.jmh:jmh-generator-annprocess:1.20'
}
Утилитный класс над которым будет производиться бенчмаркинг
Возьмем простой класс с функциями суммирования для примера:
object Utils {
fun sum(a: Int, b: Int): Int {
return a + b
}
fun sumParams(data: TestData): Int {
return data.paramA + data.paramB
}
}
Пример бенчмарка
Немного об используемых аннотациях:
@BenchmarkMode - этой аннотацией вы указываете, что и как вы хотите замерить.
Можно указать следующие режимы:
- Throughput - количество операций в единицу времени.
- AverageTime - среднее время затраченное на операцию.
- SampleTime - замеряем время потраченное на операцию, вызывается каждый раз перед бенчмаркингом.
- SingleShotTime - также замеряем время потраченное на одну операцию. Запускается один раз перед бенчмарком.Будьте готовы, что понадобиться больше прогревочных итераций.
- All - использовать все режимы.
@WarmUp - здесь вы указываете количество итераций перед прогоном вашего бенчмарка, можно также указать время на прогрев.
@Measurement - здесь вы указываете параметры для замера, параметры соврешенно такие же как и в @WarmUp, batchSize - количество вызовов вашего бенчмарк метода.
В итоге бенч на jmh будет выглядить следующим образом:
import org.openjdk.jmh.annotations.*
@BenchmarkMode(Mode.SampleTime)
@Warmup(iterations = 2)
@Measurement(iterations = 5, batchSize = 5)
open class SumBenchmark {
@Benchmark
fun sum() {
Utils.sum(2, 5)
}
}
Как разделить данные между процессами
Чаще всего возникает потребность в приготовленных данных для бенчмаркинга, но чтобы не тратить время каждый раз на приготовление, а засекать только время выполнения вашей функции, можно проинициализировать данные в функции помеченной аннотацией @Setup, эта функция будет выполняться перед стартом бенчмарка. Но кроме этого вам также пронадобиться пометить ваш класс для бенчмаркинга аннотацией @State, чтобы сказать jmh, как вы собираетесь разделять данные в вашем бенчмарке.
State может быть следующим:
-
Benchmark - данные будут разделяться между всеми рабочими потоками. Данные будут проинициализированны только одним рабочим потоком. То есть функции помеченные Setup и TearDown будет вызвана один раз.
-
Group - данные будут разделены между потоками в одной группе. То есть функции помеченные @Setup и @TearDown будут вызванны одним потоком из группы.
-
Thread - данные будут уникальны для каждого потока.
В итоге наш пример преобразуется в следующий:
import org.openjdk.jmh.annotations.*
@BenchmarkMode(Mode.SampleTime)
@Warmup(iterations = 2)
@Measurement(iterations = 5, batchSize = 5)
@State(value = Scope.Benchmark)
open class SumBenchmarkWithData {
private lateinit var test: TestData
@Setup
fun setup() {
test = TestData(2, 4)
}
@Benchmark
fun sum() {
Utils.sumParams(test)
}
}
Запускаем и получаем результаты
Запуск производиться следующим образом: ```shell script ./gradlew jmh
Результаты можно найти в build/reports/jmh/reports.txt
```shell script
Benchmark Mode Cnt Score Error Units
SumBenchmark.sum sample 5855149 ≈ 10⁻⁷ s/op
SumBenchmark.sum:sum·p0.00 sample ≈ 10⁻⁸ s/op
SumBenchmark.sum:sum·p0.50 sample ≈ 10⁻⁸ s/op
SumBenchmark.sum:sum·p0.90 sample ≈ 10⁻⁷ s/op
SumBenchmark.sum:sum·p0.95 sample ≈ 10⁻⁷ s/op
SumBenchmark.sum:sum·p0.99 sample ≈ 10⁻⁷ s/op
SumBenchmark.sum:sum·p0.999 sample ≈ 10⁻⁷ s/op
SumBenchmark.sum:sum·p0.9999 sample ≈ 10⁻⁵ s/op
SumBenchmark.sum:sum·p1.00 sample 0.001 s/op
SumBenchmarkWithData.sum sample 9676370 ≈ 10⁻⁷ s/op
SumBenchmarkWithData.sum:sum·p0.00 sample ≈ 10⁻⁸ s/op
SumBenchmarkWithData.sum:sum·p0.50 sample ≈ 10⁻⁸ s/op
SumBenchmarkWithData.sum:sum·p0.90 sample ≈ 10⁻⁷ s/op
SumBenchmarkWithData.sum:sum·p0.95 sample ≈ 10⁻⁷ s/op
SumBenchmarkWithData.sum:sum·p0.99 sample ≈ 10⁻⁷ s/op
SumBenchmarkWithData.sum:sum·p0.999 sample ≈ 10⁻⁷ s/op
SumBenchmarkWithData.sum:sum·p0.9999 sample ≈ 10⁻⁵ s/op
SumBenchmarkWithData.sum:sum·p1.00 sample 0.002 s/op
FAQ
Плагин для jmh испытывает проблемы с очисткой скомпиленного кода, решение есть следующее:
shell script
./gradlew --no-daemon clean jmh
Заключение
Если вдруг у вас с вашим коллегой возникнет спор, что же быстрее, то измерьте это как настоящие профессионалы.