文档目录

2.8 配套代码:JMH 与 kotlinx-benchmark 的完整配置

对应小节:2.8 JMH 与 kotlinx-benchmark 这份配置可以直接抄进你的项目。

一、方案 A:JMH(功能最全)

1.1 构建配置

// build.gradle.kts
plugins {
    kotlin("jvm") version "2.0.20"
    kotlin("kapt") version "2.0.20"                 // ← Kotlin 用 JMH 必须
    id("me.champeau.jmh") version "0.7.2"
}

repositories { mavenCentral() }

dependencies {
    jmh("org.openjdk.jmh:jmh-core:1.37")
    jmh("org.openjdk.jmh:jmh-generator-annprocess:1.37")
    kaptJmh("org.openjdk.jmh:jmh-generator-annprocess:1.37")   // ← 关键,缺了会静默失败
}

// 基准源码放在 src/jmh/kotlin,与 main/test 分开

⚠️ 两个 Kotlin 专属的坑:① 少了 kotlin("kapt") 和 kaptJmh,JMH 会报「找不到基准」而不提示原因;② 被测类和被测方法必须 open(Kotlin 默认 final,JMH 无法生成子类)。

1.2 基准代码

// src/jmh/kotlin/bench/SumBenchmark.kt
package bench

import org.openjdk.jmh.annotations.*
import org.openjdk.jmh.infra.Blackhole
import java.util.concurrent.TimeUnit
import kotlin.random.Random

@State(Scope.Benchmark)
@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@Warmup(iterations = 3, time = 1)
@Measurement(iterations = 5, time = 1)
@Fork(2)
@Threads(1)
open class SumBenchmark {

    private lateinit var data: IntArray

    @Setup(Level.Trial)
    fun setup() {
        val rnd = Random(42)
        data = IntArray(4096) { rnd.nextInt() }        // 运行时随机输入
    }

    @Benchmark
    fun sumForLoop(bh: Blackhole) {
        var s = 0L
        for (v in data) s += v
        bh.consume(s)                                  // 消费结果
    }

    @Benchmark
    fun sumWhileLoop(bh: Blackhole) {
        var s = 0L
        var i = 0
        while (i < data.size) { s += data[i]; i++ }
        bh.consume(s)
    }
}

1.3 运行与读输出

./gradlew jmh

# 结果在 build/reports/jmh/ 下
cat build/reports/jmh/results.txt
Benchmark                  Mode  Cnt   Score    Error   Units
SumBenchmark.sumForLoop   thrpt    2  16.234           ops/ms
SumBenchmark.sumWhileLoop thrpt    2  16.180           ops/ms
SumBenchmark.sumForLoop:gc.alloc.rate   2   0.001     MB/sec

四个必须看的字段:

字段 判读
Score 注意单位:ops/ms = 每秒 16234 次;单次耗时 ≈ 61.6 μs
Error Error / Score > 5% → 数据不稳定,先查环境
Cnt 有效轮数;小于设定值说明有轮次失败
gc.alloc.rate 分配速率,最容易被忽略、信息量最大

1.4 常用命令行覆盖

# 覆盖注解里的配置(便于快速试参数)
./gradlew jmh -PjmhFork=3 -PjmhWarmupIterations=5 -PjmhIterations=10

# 打开 GC profiler
./gradlew jmh -PjmhProfilers=gc

# 只跑某个基准
./gradlew jmh -PjmhIncludes="SumBenchmark.sumForLoop"

# 加 JVM 参数(对比不同 GC)
./gradlew jmh -PjmhJvmArgs="-XX:+UseZGC -Xmx4g"

二、方案 B:kotlinx-benchmark(更适合 CI)

2.1 构建配置

// build.gradle.kts
plugins {
    kotlin("jvm") version "2.0.20"
    id("org.jetbrains.kotlinx.benchmark") version "0.4.11"
}

dependencies {
    implementation("org.jetbrains.kotlinx:kotlinx-benchmark-runtime:0.4.11")
}

benchmark {
    targets { register("main") }
    configurations {
        register("main") {
            warmups = 3
            iterations = 5
            iterationTime = 1
            iterationTimeUnit = "s"
            mode = "thrpt"        // thrpt=吞吐, avgt=平均耗时, sample=采样
            outputTimeUnit = "ms"
        }
    }
}

2.2 基准代码(注意:不需要 open)

// src/main/kotlin/bench/SumBenchmark.kt
package bench

import kotlinx.benchmark.*
import kotlin.random.Random

@State(Scope.Benchmark)
class SumBenchmark {

    private lateinit var data: IntArray

    @Setup
    fun setup() {
        val rnd = Random(42)
        data = IntArray(4096) { rnd.nextInt() }
    }

    @Benchmark
    fun sumForLoop(bh: Blackhole) {         // Blackhole 需要 0.4.9+
        var s = 0L
        for (v in data) s += v
        bh.consume(s)
    }
}

2.3 运行与输出

./gradlew benchmark        # 任务名随插件版本略有差异

# 结果默认在 build/reports/benchmarks/,格式为结构化 JSON
ls build/reports/benchmarks/
cat build/reports/benchmarks/main.json
{
  "benchmarks": [
    {
      "name": "bench.SumBenchmark.sumForLoop",
      "mode": "thrpt",
      "score": 16234.5,
      "scoreUnit": "ops/s"
    }
  ]
}

这个 JSON 就是第 8 章 CI 性能门禁的输入——用脚本对比当前结果与仓库里的基线,超过阈值就失败。

三、两种方案的选择

需求 选择
输出 JSON、接入 CI 回归 kotlinx-benchmark
需要 @CompilerControl、自定义 profiler、多 JVM 参数对比 JMH
团队不熟悉 JMH,想要更少的坑(不需要 open/kapt) kotlinx-benchmark
需要最大的社区资料与示例 JMH

可以两者都用:日常快速比较用 kotlinx-benchmark,深度诊断用 JMH。但不要同一份基准两处维护——结果会不一致,而且没人知道哪个是对的。

四、什么时候不该用它们

场景 为什么不适合 用什么
Repository / 数据访问 JMH 的迭代模型不适合 IO 组件基准:自己写采样循环,报告百分位(第 3 章)
HTTP 接口 需要真实网络与并发 k6(第 4 章)
「端到端用户体验」 需要完整链路 负载测试(第 3、4 章)
需要观察 GC 与并发的交互 微基准隔离掉了这些 组件基准 / 集成基准

判断标准:如果被测操作有外部状态或不确定性延迟,JMH 的「多轮取平均」就不合适——你要的是延迟分布,不是平均值。

五、动手改造

改动 观察什么
删掉 kaptJmh 那一行再跑 JMH 会报「找不到基准」——记住这个症状
把 open class 改成 class 再跑 同上(另一个常见的卡点)
把 @Fork(2) 改成 @Fork(1) Error 会变大——说明单次 fork 的结果不够稳
把 @Setup(Level.Trial) 改成 Level.Invocation Score 明显变差——setup 开销被算进去了
给两个基准分别加 -PjmhJvmArgs="-XX:+UseG1GC" 和 -XX:+UseZGC 对比同一代码在不同 GC 下的表现(第 7 章会用到这个手法)
把 Random(42) 换成固定的 IntArray(4096) { 1 } 数字会变得可疑(常量折叠 + 分支预测)

六、这段代码的局限

  • JMH 的数字只在同一台机器、同一配置下可比。跨机器的绝对值没有意义。
  • CI 环境不适合跑这些基准(共享 runner 噪声大)。第 8 章会讲怎么在 CI 里做「相对基线」的比较。
  • 微基准的结论不能直接推广到系统层面。一个方法快 2 倍,可能对接口 P99 毫无影响——要看它在总延迟里的占比(第 6 章)。