Захотелось осветить тему для коллег с микробенчмаркингом в 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'
}

Ссылка на плагин github

Утилитный класс над которым будет производиться бенчмаркинг

Возьмем простой класс с функциями суммирования для примера:

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

Заключение

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

Ссылка на готовый проект