不要猜,要测试!—Java 微基准测试框架 JMH

在以前,想要知道自己写的程序性能和程序运行时间,一般都是定义一个startTime=System.nanoTime,再定义一个endTime=System.nanoTime,最后他们之间的差便是程序的运行时间。

但是,这样的测试仅仅是粗略的测试此段代码在此时的运行时间,结果并不一定可靠。这里建议使用一个更加官方的测试工具:JMH–Java微基准测试框架

Code Tools: jmh

JMH是由OpenJDK官方发布的一种Java工具,用于构建,运行和分析用Java和其他语言编写的针对JVM的nano / micro / milli / macro基准测试。

官方地址:https://openjdk.java.net/projects/code-tools/jmh/

使用JMH,你可以:

  • 当需要优化一个方法的时候,可以使用JMH测试方法运行时间,测试是否达到优化效果
  • 当需要知道一个方法大概的执行时间的时候
  • 当需要知道不同的参数对同一个方法的运行效率的影响的时候

总之,JMH在方法级别的运行时间测试上得到的结果是比较权威的。

Go Start

官方推荐JMH应该使用Maven构建。

  • 当我们仅仅需要测试一个独立的方法的时候,我们可以直接构建一个JMH Maven骨架(archetype)
mvn archetype:generate \ 
          -DinteractiveMode = false \ 
          -DarchetypeGroupId = org.openjdk.jmh \ 
          -DarchetypeArtifactId = jmh- java -benchmark-archetype \ 
          -DgroupId = org.sample \ 
          -DartifactId = test \ 
          -Dversion = 1.0
  • 当我们想在已有到的项目的基础上进行JMH测试的时候,我们可以直接添加Maven依赖:
    <!-- https://mvnrepository.com/artifact/org.openjdk.jmh/jmh-core -->
    <dependency>
      <groupId>org.openjdk.jmh</groupId>
      <artifactId>jmh-core</artifactId>
      <version>{jmh.version}</version>
    </dependency>
    
    <!-- https://mvnrepository.com/artifact/org.openjdk.jmh/jmh-generator-annprocess -->
    <dependency>
      <groupId>org.openjdk.jmh</groupId>
      <artifactId>jmh-generator-annprocess</artifactId>
      <version>1.21</version>
      <scope>{jmh.version}</scope>
    </dependency>
    
    
    

添加完依赖后,我们就可以编写性能测试方法,这里和我们经常使用的单元测试方法差不多:

这里我们测试下HashMap中,使用位运算代替取模运行的效率到底有没有提升

@BenchmarkMode(Mode.Throughput)
@Warmup(iterations = 3)
@Measurement(iterations = 10, time = 5, timeUnit = TimeUnit.SECONDS)
@Threads(8)
@Fork(2)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@State(Scope.Benchmark)
public class Test {

    @Param({"18","2035","12345"})
    private int param;

    @Benchmark
    public  void test(){
        int c=(16-1)¶m;
    }

    @Benchmark
    public  void  test2(){
       int c=param%16;
    }

    @Setup
    public void prepare() {
    }
}

执行:

执行方式分两种

  • 由于JMH测试一般测试好几轮取平均值,因此有时候测试时间比较长,这个时候我们可以使用Maven将测试打包,然后放在服务器上测试:

    mvn clean package

    然后会在对应的目录/target下生成jar包,直接

    java -jar xxx.jar

    运行即可。

  • 对于比较小的项目,我们可以直接编写Main方法,在编译器上运行即可:

      public static void main(String[] args)throws Exception {
          Options options = new OptionsBuilder()
                  .include(Test.class.getSimpleName())
                  .output("G:/Benchmark.log")
                  .build();
          new Runner(options).run();
      }
    

    这里我们指定了测试类和日志输出路径。

    执行完成后可以获取测试结果:

    # JMH version: 1.21
    # VM version: JDK 1.8.0_181, Java HotSpot(TM) 64-Bit Server VM, 25.181-b13
    # VM invoker: H:\java\jdk1.8\jre\bin\java.exe
    # VM options: -Dvisualvm.id=789216946682180 -javaagent:H:\idea\IntelliJ IDEA 2018.2.2_2\lib\idea_rt.jar=64857:H:\idea\IntelliJ IDEA 2018.2.2_2\bin -Dfile.encoding=UTF-8
    # Warmup: 3 iterations, 10 s each
    # Measurement: 10 iterations, 5 s each
    # Timeout: 10 min per iteration
    # Threads: 8 threads, will synchronize iterations
    # Benchmark mode: Throughput, ops/time
    # Benchmark: com.dengchengchao.Test.test
    # Parameters: (param = 18)
    ...
    ...
    ...
    
    Benchmark   (param)   Mode  Cnt         Score        Error   Units
    Test.test        18  thrpt   20  11201407.357 ± 288038.173  ops/ms
    Test.test      2035  thrpt   20  11155849.134 ± 411417.605  ops/ms
    Test.test     12345  thrpt   20  11242387.836 ± 124937.944  ops/ms
    Test.test2       18  thrpt   20  10618259.409 ± 166619.563  ops/ms
    Test.test2     2035  thrpt   20  10566613.381 ± 185395.887  ops/ms
    Test.test2    12345  thrpt   20  10497633.170 ± 226031.760  ops/ms
    

    可以发现,结果中说明了每个方法的平均吞吐量以及误差范围等,我们可以得到结果:在我的电脑中,JDK 1.8环境下,使用Java 的位运算和取模运行时间相差不大,

在这里,使用JMH测试方法运行就算完成了,其实也比较简单,并且比自己写System.nanoTime更加准确。

注解介绍

在上面的方法测试中,我们发现了很多新的注解。下面对上这些注解进行简单介绍:

@Benchmark(测试方法标记注解)

标记测试方法,和@Test注解差不多,标注后在运行的时候JMH将会对此 方法进行测试。

官方的HelloWorld例子仅仅使用了这一个注解,因为这里的注解其实也是一种启动参数,不仅仅可以通过注解表示,还可以使用类似java -jar xxx.jar -f 1 -t 2方式指定,也可以在编写Main方法的时候,使在构建方法Builder的时候指定。这种灵活的参数指定方式,可以在不同的场景的灵活应用。

@BenchmarkMode(基准测试类型)

  • Throughput : 吞吐量,比如:1s内可以执行多少次调用
  • AverageTime: 调用的平均时间
  • SampleTIme: 随机取样,最后输出取样结果分布,例如“99%的调用在xxx毫秒以内,99.99%的调用在xxx毫秒以内”
  • SingleShotTime:以上模式都是默认一次 iteration 是 1s,唯有 SingleShotTime 是只运行一次。往往同时把 warmup 次数设为0,用于测试冷启动时的性能。
  • All:执行所有的类型测试

@Measurement(基本测试参数)

  • iterations 进行测试的轮次

  • time 每轮进行的时长

  • timeUnit 时长单位

@Warmup

基准测试预热选项,有时候在JVM的优化下(比如JIT),前几次测试的结果一般不准确,因此我们可以指定预热时间,将前几次的测试结果丢弃。@Warmup的选项和@Measurement是相同的

@Threads

每个进程中的测试线程,根据具体情况选择,一般为cpu乘以2。

@Fork

启动测试进程数,比如@Fork(2),那么JMH会启动两个进程进行测试

@OutputTimeUnit

基准测试时间单位,比如秒,毫秒,微秒等

@Param

指定参数,可以用来测试在不同参数的情况下方法的运行效率

@Setup

方法级注解,被标记的方法会在启动前运行,一般用来在测试之前进行一些准备工作,比如数据初始化等

@TearDown

方法级注解,被标记的方法会在测试完成后运行,一般用来在测试之后的一些清理工作,比如关闭连接等

@State

当使用@Setup参数的时候,必须在类上加这个参数,不然会提示无法运行。

State 用于声明某个类是一个“状态”,然后接受一个 Scope 参数用来表示该状态的共享范围。 因为很多 benchmark 会需要一些表示状态的类,JMH 允许你把这些类以依赖注入的方式注入到 benchmark 函数里。Scope 主要分为三种。

  • Thread: 该状态为每个线程独享。

  • Group: 该状态为同一个组里面所有线程共享。

  • Benchmark: 该状态在所有线程间共享。

官方文档中,有个比较好的例子来展示@state的使用:JMHSample_03_States

在JMH能做的,远不止这些,想要了解更多,可以阅读官方文档

参考文章:

JMH简介-ImportNew

Java微基准测试框架JMH

JMH官方文档

[译]使用JMH进行微基准测试:不要猜,要测试!