You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

为何JMH对不同方法输出相同结果?基准测试失效原因咨询

JMH基准测试结果失真的原因与解决方案

这个问题我之前排查过好几次,核心原因是JVM的JIT即时编译器做了死代码消除(Dead Code Elimination)优化,直接把你写的业务逻辑给“优化没了”,导致两个测试的实际执行逻辑几乎一致。

为什么会出现这种情况?

JIT编译器在运行时会深度分析代码的实际用途,当它发现一段代码的计算结果完全没有被后续逻辑使用时,就会判定这段代码是“死代码”,直接跳过执行——毕竟执行没用的代码只会浪费CPU资源。

看你的第二个测试案例:

  • 你创建了copy列表,还循环把list的元素添加进去,但之后这个copy变量完全没有被引用(既没有返回,也没有作为参数传递给其他方法,更没有修改外部状态)
  • JIT一眼就看穿了:“这个copy根本没用,那创建它和循环添加元素的代码都是白做的,不如直接删掉”
  • 最终你的run()方法实际执行的逻辑和第一个空方法几乎一样:只返回固定字符串"done",所以耗时自然相同。

怎么解决这个问题?

要让JMH的测试结果真实有效,核心就是让被测代码的结果被“实际使用”,让JIT认为这段代码是有价值的。这里有几个常用的解决方案:

1. 返回业务逻辑的结果

把方法的返回值从固定字符串改成你业务逻辑生成的对象,这样JIT就不会消除这段代码:

public class MyBenchmark {
    public static void main(String[] args) throws Exception {
        for (int i = 0; i < 10000000; i++) {
            list.add(i);
        }
        org.openjdk.jmh.Main.main(args);
    }
    private static List<Integer> list = new ArrayList<>();
    @Fork(value = 1, warmups = 0)
    @Benchmark
    @BenchmarkMode(Mode.AverageTime)
    @Warmup(iterations = 5)
    public List<Integer> run() { // 修改返回值类型
        List<Integer> copy = new ArrayList<>();
        for (Integer item : list) {
            copy.add(item);
        }
        return copy; // 返回业务逻辑生成的对象
    }
}

2. 使用JMH的@State注解存储结果

如果你的方法不需要返回值,可以把结果存储到一个被@State注解标记的对象中,确保JVM认为这个结果会被后续使用:

@State(Scope.Benchmark)
public class MyBenchmark {
    private List<Integer> list = new ArrayList<>();
    private List<Integer> result; // 用于存储业务逻辑结果

    @Setup
    public void setup() {
        // 初始化数据放在@Setup方法里,这是JMH的最佳实践
        for (int i = 0; i < 10000000; i++) {
            list.add(i);
        }
    }

    public static void main(String[] args) throws Exception {
        org.openjdk.jmh.Main.main(args);
    }

    @Fork(value = 1, warmups = 0)
    @Benchmark
    @BenchmarkMode(Mode.AverageTime)
    @Warmup(iterations = 5)
    public void run() {
        List<Integer> copy = new ArrayList<>();
        for (Integer item : list) {
            copy.add(item);
        }
        result = copy; // 把结果赋值给状态变量
    }
}

(另外提一句:初始化数据放在@Setup方法里是JMH的最佳实践,比在main里初始化更规范)

3. 禁用特定JIT优化(不推荐)

你可以通过JMH的@CompilerControl注解禁用死代码消除,但这种方法会破坏JVM的正常优化,测试结果可能无法反映真实的运行情况,所以只适合排查问题时临时使用:

@CompilerControl(CompilerControl.Mode.DCE) // 禁用死代码消除
@Benchmark
public String run() {
    // 原业务逻辑代码
}

补充:JMH基准测试的核心原则

在编写JMH测试时,一定要记住:被测代码的所有计算结果必须被“可见地使用”——要么返回,要么修改外部状态,要么传递给其他必须执行的方法。否则JIT的各种优化(死代码消除、循环展开、方法内联等)都会让测试结果失真。

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

火山引擎 最新活动