JMH基准测试:OperationsPerInvocation与单次add结果差异原因咨询
为什么JMH测试ArrayList.add()时单次调用和循环批量调用的百分位结果差异巨大?
你的猜测完全正确,核心原因就是JMH的框架调用开销被平摊的程度不同,再加上CPU执行效率的差异,最终导致了两个测试结果的巨大差距,具体拆解如下:
1. JMH框架开销的核心影响
ArrayList的add()方法在未触发扩容时是极快的O(1)操作,执行时间仅几纳秒。但JMH每次调用基准方法时,都会产生额外开销:比如State对象的参数注入、Blackhole的消费逻辑、方法调用栈的创建销毁、线程状态维护等。
- 对于单次add测试(代码示例2):每次基准方法调用只执行一次
add(),JMH的框架开销会直接计入这次操作的时间统计。尤其是99.99%这类高百分位,会捕捉到框架开销带来的长尾延迟——比如偶尔的线程调度、JVM内部小停顿等,直接拉高了单次操作的百分位数值。 - 对于批量循环测试(代码示例1):JMH仅需调用一次基准方法就能执行1亿次
add(),框架开销被平摊到每一次操作上,几乎可以忽略。此时统计到的百分位,反映的是add()操作本身的真实执行延迟。
2. CPU执行效率的额外差异
批量循环的调用方式还有一个优势:连续的add()操作形成了规律的指令流,CPU的分支预测(ArrayList扩容判断分支绝大多数时候不会触发)、缓存命中率(数组元素的缓存复用)都会更高,流水线执行效率更优,进一步降低了add()操作的实际执行时间,让百分位数值更低。
代码示例
代码示例1:批量循环调用
@Benchmark public void addItems(ThreadState state, Blackhole blackhole) { blackhole.consume(addItems(state.list, state.items, state.value)); } private static boolean addItems(List<Long> list, int items, long value) { for (int i = 0; i < items; i++) { list.add(value); } return true; }
代码示例2:单次调用
@Benchmark public void add(ListState listState, Blackhole blackhole) { listState.list.add(listState.val); }
建议
如果想要准确测量单个add()操作的真实性能,推荐使用批量循环+OperationsPerInvocation的方式,同时确保JMH的预热配置(@Warmup)足够充分,让JVM完成代码编译、缓存预热等优化。另外,统计百分位时要注意区分“基准方法调用时间”和“单个操作时间”,避免被框架干扰。
内容的提问来源于stack exchange,提问作者Abidi
相关产品推荐
相关产品推荐

