更準確的測試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是單次執行的時間

​ 從結果上看,兩種排序演算法的性能相差無幾,當然與我們的邏輯太簡單也有關係。這次的分享就到這裡,大家趕緊用到自己的項目中,測試一下吧。