You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Java微基准测试框架JMH中AverageTime与SingleShotTime的区别及配置疑问

JMH SingleShotTime模式行为疑问

已理解的JMH模式区别

我已明确JMH中AverageTime与Throughput模式的差异:

假设基准方法带有如下注解:

@Benchmark
@Fork(value = 1, warmups = 0)
@Measurement(iterations = 1, time = 1, timeUnit = TimeUnit.SECONDS)
  • 使用@BenchmarkMode(Mode.AverageTime)时:JMH启动1个fork,执行1次迭代,在至少1秒内尽可能多次调用基准方法(即Level.Invocation级别),最终通过总耗时÷调用次数得到单次调用的平均时间。
  • 使用@BenchmarkMode(Mode.Throughput)时:逻辑类似,但最终通过完成调用次数÷总耗时计算吞吐量。

问题场景:SingleShotTime模式的不符合预期行为

我编写了一个带状态的基准测试类,预期SingleShotTime模式下仅调用一次基准方法并测量其耗时,但实际出现了多次调用,且多线程共用状态实例导致冲突:

@State(Scope.Benchmark)
internal open class FooBenchmark {
    class Foo {
        private val values = mutableListOf<String>()

        fun add() = values.add(values.size.toString())

        fun size() = values.size
    }

    private val foo = Foo()

    @Benchmark
    @Fork(value = 1, warmups = 0)
    @Measurement(iterations = 1, time = 1, timeUnit = TimeUnit.SECONDS)
    @BenchmarkMode(Mode.SingleShotTime)
    fun fooBenchmark(hole: Blackhole) {
        check(foo.size() == 0)
        foo.add()
        check(foo.size() == 1)
        hole.consume(foo)
    }
}

运行基准测试时,JMH输出如下:

> Task :lib:runBenchmark
# Detecting actual CPU count: 8 detected
# JMH version: 1.37
# VM version: JDK 19.0.1, OpenJDK 64-Bit Server VM, 19.0.1+10-21
# VM invoker: /opt/jdk-19.0.1/bin/java
# VM options: -Dfile.encoding=UTF-8 -Duser.country=US -Duser.language=en -Duser.variant
# Blackhole mode: compiler (auto-detected, use -Djmh.blackhole.autoDetect=false to disable)
# Warmup: <none>
# Measurement: 1 iterations, 1 s each
# Timeout: 10000 ms per iteration
# Threads: 8 threads
# Benchmark mode: Single shot invocation time
# Benchmark: org.kepocnhh.jmh.FooBenchmark.fooBenchmark

实际运行中,JMH会多次调用fooBenchmark方法,且8个线程共用同一个FooBenchmark实例(因Scope.Benchmark),导致foo状态的并发访问冲突,触发check断言失败。我不确定是对模式原理理解有误,还是配置存在问题。


问题原因与解决方法

核心原因

  1. 默认线程数导致多线程调用:JMH默认线程数等于CPU核心数(输出中显示8 threads),每个线程都会独立执行基准方法,这直接导致多次调用。
  2. 测量时间配置导致持续调用:@Measurement(time = 1)表示迭代会持续1秒,在这段时间内,每个线程会尽可能多次调用基准方法——SingleShotTime模式的核心是测量单次调用的耗时,但它仍然会在配置的迭代时间/次数内重复调用以获取统计样本,而非只调用一次。

实现“仅调用一次基准方法”的正确配置

要达到“仅调用一次基准方法并测量耗时”的需求,需调整两个配置:

  • 指定线程数为1:添加@Threads(1)注解,避免多线程并发调用
  • 调整测量方式为“调用次数”而非“持续时间”:将@Measurement的time参数替换为count = 1,表示每次迭代仅调用1次基准方法

修改后的基准方法注解示例:

@Benchmark
@Fork(value = 1, warmups = 0)
@Measurement(iterations = 1, count = 1) // 每次迭代仅调用1次
@Threads(1)
@BenchmarkMode(Mode.SingleShotTime)
fun fooBenchmark(hole: Blackhole) {
    check(foo.size() == 0)
    foo.add()
    check(foo.size() == 1)
    hole.consume(foo)
}

补充说明

  • Scope.Benchmark表示整个基准测试共享一个状态实例,若不需要多线程共享,可改为Scope.Thread,每个线程拥有独立的状态实例,避免并发冲突;但如果只是要单次调用,@Threads(1)已经足够。
  • SingleShotTime适合测量单次操作的绝对耗时,比如冷启动场景,此时搭配count=1和Threads(1)才能实现“单次调用测量”的预期。

内容的提问来源于stack exchange,提问作者Stanley Wintergreen

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.12 01:20:17