更准确的测试Java程序性能——JMH基准测试

什么是JMH

​ JMH,即Java Microbenchmark Harness,Java平台下的一套微基准测试工具。如果我们需要测试API性能的话,就可以用上这个工具,所以它并不是取代单元测试的。它可以在开发阶段提供性能参考标准,不过这并不代表线上的性能表现,不同的硬件和操作系统也会带来性能差异,所以最终还是需要上到测试或沙箱环境,让测试人员进行压测。

为什么需要JMH

​ 在了解JMH之前,如果需要性能测试,我们通常会使用for循环,或者JMeter。而JMH正是比for循环严谨,比JMeter使用简单的测试工具。

​ 再者,不知道你注意过没有,在使用for循环测试时,第一次或者头几次运行总是最慢的,越到后面越快。从《计算机组成与设计 硬件软件接口》一书中可以了解到,从更底层讲,Java是解释型的语言。虽然Java也需要编译,但是编译后只是字节码,还需要JVM解释成对应宿主机的机器码。解释的优势是可移植性,但是性能较差。在20世纪80和90年代,虽然解释型语言的性能也飞速提升,但是与C语言相比,仍有10倍的性能差距。

为了保持可移植性,同时又提高性能,Java便开发了即时编译器(Just In Time complier),其通过记录运行的程序来找到所谓的“热点”方法,然后将它们直接编译成宿主机的指令序列,即不通过JVM解释那一层。这样以后该方法的运行就会更快。

​ 看到这里也就明白了,为什么程序越到后面就会越快。JMH在真正的测试之前会预热程序,而且还可以通过配置进程数、线程数等参数来使程序更接近实际的运行状况。

如何使用

​ 首先引入Maven依赖:

	<dependency>
            <groupId>org.openjdk.jmh</groupId>
            <artifactId>jmh-core</artifactId>
            <version>1.21</version>
        </dependency>
        <dependency>
            <groupId>org.openjdk.jmh</groupId>
            <artifactId>jmh-generator-annprocess</artifactId>
            <version>1.21</version>
            <scope>test</scope>
        </dependency>

​ 本案例中,我写了一个简单的小程序,它会从指定目录读取文件夹内容(每行一个数字),然后会对取出来的数字进行排序。排序算法选择了插入排序和归并排序,我们通过基准测试来看看两者的性能差距。

​ 读取文件内容

public class ReadFile {
   public static int[] readInteger(String path){
       try(BufferedReader in = new BufferedReader(new FileReader(path));) {
           List<Integer> temp = new ArrayList<>();
           String str;
           while ((str = in.readLine()) != null) {
               temp.add(Integer.parseInt(str));
           }
           int[] result = new int[temp.size()];
           for(int i=0;i<temp.size();i++){
               result[i]=temp.get(i);
           }
           return result;
       } catch (Exception e) {
           e.printStackTrace();
           return new int[0];
       }
   }
}

​ 两个排序算法就不贴了,网上可以搜到很多。实际的开发可能会用上SpringBoot,所以还得与Junit整合,并使用自动注入功能。先直接贴上测试代码:

@BenchmarkMode(Mode.All)
@Warmup(iterations = 3)//预热轮数
@Measurement(iterations = 1, time = 1, timeUnit = TimeUnit.SECONDS)
@Threads(8)//线程数
@Fork(0) //fork的次数,如果想用Autowired自动注入,这个填0
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@State(Scope.Benchmark)
@RunWith(SpringRunner.class) //整合SpringBoot的测试运行环境
@SpringBootTest
public class JHMTest {
  	//想用自动注入功能,对象必须是静态的,fork填0
    private static SortService service;

    @Autowired
    void setService(SortService service){
        JHMTest.service =service;
    }

    @Test
    public void executeBenchmark() throws RunnerException {
        //JMH的选项配置,除了上面的注解方式的配置,也可以直接在这个Options里面配置。
      	//其中/Users/xxxx/Desktop/Benchmark.json是结果的输出文件
        Options options = new OptionsBuilder().include(this.getClass().getSimpleName())
                .output("/Users/xxxx/Desktop/Benchmark.json").build();
        new Runner(options).run();
    }

    @Benchmark
    public void insertSortTest(){
        int[] arr = ReadFile.readInteger("/Users/xxxxx/Desktop/test.txt");
        service.insertSort(arr);
    }

    @Benchmark
    public void mergeSortTest(){
        int[] arr = ReadFile.readInteger("/Users/xxxxx/Desktop/test 2.txt");
        service.mergeSort(arr);
    }
}

​ 上面注释简单写了几个关键点,我们执行executeBenchmark方法,JMH就会执行该类下带有Benchmark注解的方法。最终结果会输出到指定文件中

​ 其他注解的解释可见图

截图截取自://dunwu.github.io/javatech/test/jmh.html#jmh-api

结果查看

​ 打开结果文件,前面一大坨是系统信息,可以简单看看,直接拉到最后,结果如下:

Benchmark                                        Mode     Cnt    Score   Error   Units
JHMTest.insertSortTest                          thrpt          129.302          ops/ms
JHMTest.mergeSortTest                           thrpt          122.224          ops/ms
JHMTest.insertSortTest                           avgt            0.065           ms/op
JHMTest.mergeSortTest                            avgt            0.066           ms/op
JHMTest.insertSortTest                         sample  122410    0.066 ± 0.002   ms/op
JHMTest.insertSortTest:insertSortTest·p0.00    sample            0.014           ms/op
JHMTest.insertSortTest:insertSortTest·p0.50    sample            0.050           ms/op
JHMTest.insertSortTest:insertSortTest·p0.90    sample            0.106           ms/op
JHMTest.insertSortTest:insertSortTest·p0.95    sample            0.120           ms/op
JHMTest.insertSortTest:insertSortTest·p0.99    sample            0.192           ms/op
JHMTest.insertSortTest:insertSortTest·p0.999   sample            0.492           ms/op
JHMTest.insertSortTest:insertSortTest·p0.9999  sample           11.891           ms/op
JHMTest.insertSortTest:insertSortTest·p1.00    sample           17.334           ms/op
JHMTest.mergeSortTest                          sample  122055    0.066 ± 0.002   ms/op
JHMTest.mergeSortTest:mergeSortTest·p0.00      sample            0.014           ms/op
JHMTest.mergeSortTest:mergeSortTest·p0.50      sample            0.050           ms/op
JHMTest.mergeSortTest:mergeSortTest·p0.90      sample            0.107           ms/op
JHMTest.mergeSortTest:mergeSortTest·p0.95      sample            0.121           ms/op
JHMTest.mergeSortTest:mergeSortTest·p0.99      sample            0.187           ms/op
JHMTest.mergeSortTest:mergeSortTest·p0.999     sample            0.457           ms/op
JHMTest.mergeSortTest:mergeSortTest·p0.9999    sample           11.957           ms/op
JHMTest.mergeSortTest:mergeSortTest·p1.00      sample           12.419           ms/op
JHMTest.insertSortTest                             ss            0.020           ms/op
JHMTest.mergeSortTest                              ss            0.020           ms/op

​ 结果如上,Mode中thrpt代表吞吐量,单位时间内的执行次数。avgt是平均时间,一次执行需要的单位时间。sample是基于采样的执行时间,采样频率由JMH自动控制。ss是单次执行的时间

​ 从结果上看,两种排序算法的性能相差无几,当然与我们的逻辑太简单也有关系。这次的分享就到这里,大家赶紧用到自己的项目中,测试一下吧。