为何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




