JMH:在所有基准测试中使用相同的静态对象

JMH: Using the same static object in all Benchmark tests

我有一个 class 可以构建一些复杂的数据(想象一个大的 XML 或 JSON 结构 - 那种东西)。建造它需要时间。所以我想构造一次,然后在所有测试中使用相同的数据。目前我基本上在定义 main 的 class 中定义了一个 public static 对象实例,然后在测试中明确引用它(代码是一个非常简化的示例):

public class Data 
{
    // This class constructs some complicated data 
}

public class TestSet 
{
    public static final Data PARSE_ME = new Data(...);

    public static void main(String[] args) throws RunnerException 
    {
        Options opt = new OptionsBuilder()
                .include(".*ParserTest") // several tests
                .forks(1)
                .build();

        new Runner(opt).run();
    }
}

@State(Scope.Thread)
public class SomeParserTest
{
    @Setup(Level.Iteration)
    public void setup()
    {
        Parser parser = new Parser(TestSet.PARSE_ME);
    }

    @Benchmark
    public void getId() 
    {
        parser.getId(123);
    }
}

这当然很糟糕......一个同样邪恶的选择是创建一个单独的 class 只是为了它可以容纳一个静态对象。使用像

这样的东西会很好
Options opt = new OptionsBuilder()
    ...
    .param(/*my Data object comes here*/)

param 只接受字符串,所以不确定如何将对象(更重要的是:对象的同一实例!)传递给它。

那么有什么比我上面描述的全局对象更优雅的吗?

我只想将您所有的基准测试方法移动到一个 class 中,将您的状态对象定义为内部 class,然后将状态注入到每个方法中:

public class ParserBenchmarks {

  @State(Scope.Thread)
  public static class StateHolder {
    Parser parser = null;

    @Setup(Level.Iteration)
    public void setup()
    {
      parser = new Parser(TestSet.PARSE_ME);
    }

    public Parser getParser() {
      return parser;
    }
  }

  @Benchmark
  public int getId_123(StateHolder stateHolder) {
    return stateHolder.getParser().getId(123);
  }

  @Benchmark
  public int getId_456(StateHolder stateHolder) {
    return stateHolder.getParser().getId(456);
  }

}

请注意,您的所有基准测试方法都应 return 值,否则编译器可能会将其作为死代码消除。

遗憾的是,JMH 无法在基准测试之间共享数据。

一方面,这打破了基准测试隔离,当一个基准测试可以静默修改另一个基准测试的输入数据,导致比较不正确。这就是为什么您需要 @Setup 每个基准测试的 @State 对象。

但更重要的是,当 JMH 将执行每个测试时,无论您构建什么技巧来在基准测试之间共享数据(例如,可从两者访问的 static 字段)都会在默认的 "forked" 模式下中断在自己的虚拟机中。值得注意的是,你用 static final Data TestSet.PARSE_ME 建议的事情实际上会为每个 @Benchmark 执行,因为每个新的 VM 实例无论如何都必须初始化 TestSet ;)当然,你可以禁用分叉,但是引入的问题多于解决的问题。

因此,花时间使设置成本更容易接受可能是一个更好的主意,这样它就不会非常痛苦。例如,反序列化磁盘中的数据而不是计算它。或者,想出一种更快的计算方法。