更準確的測試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註解的方法。最終結果會輸出到指定文件中。
其他註解的解釋可見圖
結果查看
打開結果文件,前面一大坨是系統資訊,可以簡單看看,直接拉到最後,結果如下:
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是單次執行的時間。
從結果上看,兩種排序演算法的性能相差無幾,當然與我們的邏輯太簡單也有關係。這次的分享就到這裡,大家趕緊用到自己的項目中,測試一下吧。