更準確的測試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註解的方法。最終結果會輸出到指定檔案中。
其他註解的解釋可見圖
截圖擷取自:https://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是單次執行的時間。
從結果上看,兩種排序演算法的效能相差無幾,當然與我們的邏輯太簡單也有關係。這次的分享就到這裡,大家趕緊用到自己的專案中,測試一下吧。