JMH基准测试如何保证每次List插入测试的初始状态一致
核心方案
以下两种方案都可以保证每次插入操作的初始列表状态一致,同时避免初始化开销影响测试结果:
方案1:Invocation级别TearDown回滚状态(最推荐)
JMH的@TearDown方法执行耗时不会计入基准测试统计,你可以在每次插入操作完成后,删除刚插入的元素将列表恢复到初始大小,完全解决状态不一致问题,额外开销远低于Invocation级别的列表初始化开销。
修改后的核心代码如下:
// 存储单次插入的位置,用于回滚 private final ThreadLocal<Integer> insertPos = new ThreadLocal<>(); @Benchmark public void insertFirst() { insertPos.set(0); this.target.add(0, this.insert); } @Benchmark public void insertMiddle() { int pos = this.size / 2; insertPos.set(pos); this.target.add(pos, this.insert); } @Benchmark public void insertLast() { int pos = this.target.size(); insertPos.set(pos); this.target.add(this.insert); } // 每次基准方法调用完成后执行,不计入统计耗时 @TearDown(Level.Invocation) public void rollback() { this.target.remove((int) insertPos.get()); insertPos.remove(); }
方案2:批量插入分摊初始化开销
如果担心Invocation级别的调度开销影响纳秒级统计精度,可以采用批量测试思路:
- 在
@Setup(Level.Iteration)中初始化列表,提前为ArrayList预留足够容量(如初始大小+批量插入次数),避免测试过程中触发扩容 - 每次基准方法执行固定次数(如1000次)的插入,总耗时除以批量次数即可得到单次插入的平均耗时,初始化开销会被分摊到上千次操作中,对结果影响可忽略
核心代码示例:
@Param({"1000"}) // 单次基准方法内的批量插入次数 int batchSize; @Setup(Level.Iteration) public void setup() { this.target = switch (this.type) { case "arraylist" -> new ArrayList<>(size + batchSize); // 提前预留容量 case "linkedlist" -> new LinkedList<>(); default -> throw new IllegalStateException("Unsupported list type: " + type); }; IntStream.range(0, this.size).mapToObj(Item::new).forEach(this.target::add); this.insert = new Item(-1); } @Benchmark public void insertFirst(Blackhole bh) { for (int i = 0; i < batchSize; i++) { this.target.add(0, this.insert); } bh.consume(target); }
额外优化建议
- 将
@State的scope改为Scope.Thread,避免多线程测试时并发修改列表、缓存行共享带来的性能干扰 - 不需要多线程测试时可以添加
@Threads(1)注解强制单线程执行,进一步减少无关变量影响
内容的提问来源于stack exchange,提问作者Kirill
相关产品推荐
相关产品推荐

