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 章)。