看開源項目時,時不常遇到一個叫做benchmark的目錄,此時腦子停滯,一眼帶過,最近一次看到就順手問了下谷大哥,發現benchmark還是個挺有意思的東西。
基準測試是什麼
基準測試
是指通過設計科學的測試方法、測試工具和測試系統,實現對一類測試對象的某項性能指標進行定量的和可對比的測試。
例如,對計算機CPU進行浮點運算、數據訪問的帶寬和延遲等指標的基準測試
,可以使用戶清楚地瞭解每一款CPU的運算性能及作業吞吐能力是否滿足應用程序的要求;再如對數據庫管理系統的ACID(Atomicity, Consistency, Isolation, Durability, 原子性、一致性、獨立性和持久性)、查詢時間和聯機事務處理能力等方面的性能指標進行基準測試
,也有助於使用者挑選最符合自己需求的數據庫系統。
通過基準測試
我們可以瞭解某個軟件在給定環境下的性能表現,對使用者而言可以用作選型的參考,對開發者而言可以作爲後續改進的基本參照。
JMH是什麼
JMH(Java Microbenchmark Harness)是Java用來做基準測試一個工具,該工具由openJDK提供並維護,測試結果可信度較高,該項目官方還在持續更新中。
下面只是JMH簡單描述,正所謂“紙上得來終覺淺,絕知此事要躬行”,要想全面瞭解還得讀完官方給出的Demo或者看我的翻譯註解版本的官方Demo。
已經給標題加鏈接,直接戳標題即可閃現到例子。官方例子有37個,這裏只列出了梗概。
舉個例子
功能入門第一課,Hello World!
public class JMHSample_01_HelloWorld {
@Benchmark
public void wellHelloThere() {
// this method was intentionally left blank.
}
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
// 指明本次要跑的類
.include(JMHSample_01_HelloWorld.class.getSimpleName())
// fork JVM的數量
.forks(1)
.build();
new Runner(opt).run();
}
}
看下輸出:
# JMH version: 1.21
# VM version: JDK 1.8.0_74, Java HotSpot(TM) 64-Bit Server VM, 25.74-b02
# VM invoker: /Library/Java/JavaVirtualMachines/jdk1.8.0_74.jdk/Contents/Home/jre/bin/java
# VM options: -javaagent:/Applications/IntelliJ IDEA.app/Contents/lib/idea_rt.jar=51264:/Applications/IntelliJ IDEA.app/Contents/bin -Dfile.encoding=UTF-8
# 預熱配置
# Warmup: 5 iterations, 10 s each
# 檢測配置
# Measurement: 5 iterations, 10 s each
# 超時配置
# Timeout: 10 min per iteration
# 測試線程配置
# Threads: 1 thread, will synchronize iterations
# 基準測試運行模式
# Benchmark mode: Throughput, ops/time
# 當前測試的方法
# Benchmark: com.cxd.benchmark.JMHSample_01_HelloWorld.wellHelloThere
# 運行過程的輸出
# Run progress: 0.00% complete, ETA 00:01:40
# Fork: 1 of 1
# Warmup Iteration 1: 2924740803.993 ops/s
# Warmup Iteration 2: 2916472711.387 ops/s
# Warmup Iteration 3: 3024204715.897 ops/s
# Warmup Iteration 4: 3051723946.668 ops/s
# Warmup Iteration 5: 2924014544.301 ops/s
Iteration 1: 2909665054.710 ops/s
Iteration 2: 2989675862.826 ops/s
Iteration 3: 2965046292.629 ops/s
Iteration 4: 3020263765.220 ops/s
Iteration 5: 2929485177.735 ops/s
# 當前方法測試結束的報告
Result "com.cxd.benchmark.JMHSample_01_HelloWorld.wellHelloThere":
2962827230.624 ±(99.9%) 171803440.922 ops/s [Average]
(min, avg, max) = (2909665054.710, 2962827230.624, 3020263765.220), stdev = 44616808.022
CI (99.9%): [2791023789.702, 3134630671.547] (assumes normal distribution)
# Run complete. Total time: 00:01:41
REMEMBER: The numbers below are just data. To gain reusable insights, you need to follow up on
why the numbers are the way they are. Use profilers (see -prof, -lprof), design factorial
experiments, perform baseline and negative tests that provide experimental control, make sure
the benchmarking environment is safe on JVM/OS/HW level, ask for reviews from the domain experts.
Do not assume the numbers tell you what you want them to tell.
# 所有benchmark跑完後的最終報告
Benchmark Mode Cnt Score Error Units
JMHSample_01_HelloWorld.wellHelloThere thrpt 5 2962827230.624 ± 171803440.922 ops/s
上面的報告顯示,我們用1個線程對吞吐量測量,測量和預熱分別迭代了5次,最終得出的Score(ops/s)是2962827230.624 ± 171803440.922,測量過程中沒有出現Error。
測試控制
測試輸出結果中開頭的配置項都是我們可以通過註解、編程或者命令行的方式來控制的。它可以具體到每個benchmark。
-
@Fork
需要運行的試驗(迭代集合)數量。每個試驗運行在單獨的JVM進程中。也可以指定(額外的)JVM參數。 -
@Measurement
提供真正的測試階段參數。指定迭代的次數,每次迭代的運行時間和每次迭代測試調用的數量(通常使用@BenchmarkMode(Mode.SingleShotTime)
測試一組操作的開銷——而不使用循環) -
@Warmup
與@Measurement
相同,但是用於預熱階段 -
@Threads
該測試使用的線程數。默認是Runtime.getRuntime().availableProcessors()
測試維度
通過JMH提供的工具我們可以輕鬆的的獲得某個功能的吞吐量、平均運行時間、冷啓動等指標的數據。
這些輸出結果通過@BenchmarkMode
註解來控制,它的值定義在枚舉類org.openjdk.jmh.annotations.Mode
中。
@BenchmarkMode
接受的參數是一個Mode
數據,也就是說,我們可以指定一個或者多個Mode
,測試時會把我們指定的模式依次運行,打印出結果。
public enum Mode {
/**
* <p>Throughput: operations per unit of time.</p>
*
* 計算一個時間單位內操作數量
*/
Throughput("thrpt", "Throughput, ops/time"),
/**
* <p>Average time: average time per per operation.</p>
*
* 計算平均運行時間
*/
AverageTime("avgt", "Average time, time/op"),
/**
* <p>Sample time: samples the time for each operation.</p>
*
* 計算一個方法的運行時間(包括百分位)
*/
SampleTime("sample", "Sampling time"),
/**
* <p>Single shot time: measures the time for a single operation.</p>
*
* 方法僅運行一次(用於冷測試模式)
* 或者特定批量大小的迭代多次運行(具體查看的“`@Measurement“`註解)——這種情況下JMH將計算批處理運行時間(一次批處理所有調用的總時間)
*/
SingleShotTime("ss", "Single shot invocation time"),
/**
* Meta-mode: all the benchmark modes.
* 所有模式依次運行
*/
All("all", "All benchmark modes"),
;
// 省略...
}
數據的共享
測試的時候,我們可能需要向測試方法傳入若干參數,這些參數還可能需要不同的隔離級別:每個線程單獨一份還是每個benchmark一份還是一組線程共享等。
這些參數有以下要求:
- 有無參構造函數(默認構造函數)
- 是公共類
- 內部類應該是靜態的
- 該類必須使用
@State
註解
參數必須是對象,因爲@State
被定義爲ElementType.TYPE
,即:可以用來註解類、接口或者枚舉。
@State
接受單個配置值Scope
。
public enum Scope {
/**
* <p>Benchmark state scope.</p>
*
* 運行相同測試的所有線程將共享實例。
* 可以用來測試狀態對象的多線程性能(或者僅標記該範圍的基準)。
*/
Benchmark,
/**
* <p>Group state scope.</p>
*
* 實例分配給每個線程組(查看後面的測試線程組)
*/
Group,
/**
* <p>Thread state scope.</p>
*
* 實例將分配給運行給定測試的每個線程。
*
*/
Thread,
}
數據的準備
數據的準備像極了JUnit和TestNG的方式,即:在測試開始前後分別進行處理。在JMH中對應到@Setup
和@TearDown
,我們可以在被進行標記的方法中對數據進行處理,並且處理的耗時不被記入正常測試的時間,也就是說不會影響我們測試結果。
@Setup
和@TearDown
分別接受單個配置值Level
。
public enum Level {
/**
* Trial level: to be executed before/after each run of the benchmark.
*
* 在每個benchmark之前/之後運行
*/
Trial,
/**
* Iteration level: to be executed before/after each iteration of the benchmark.
*
* 在一次迭代之前/之後(一組調用)運行
*/
Iteration,
/**
* Invocation level: to be executed for each benchmark method execution.
*
* 每個方法調用之前/之後
* 該方式較爲複雜,在沒有搞清楚之前,不要使用。
*/
Invocation,
;
}
測試線程組
我們可以通過指定線程組的的方式來模擬一些場景,比如:生產-消費。這種場景下生產消費的線程數量往往是不一致的,通過@Group
和@GroupThreads
我們可以很輕鬆的製造出這種場景。
具體的使用說明例子中說的很詳細 ----傳送門----> JMHSample_15_Asymmetric
編譯器的控制
我們知道編譯器在編譯時會做一些優化,這個例子展示瞭如何用@CompilerControl
來控制編譯對**方法內斂**優化的控制。
public @interface CompilerControl {
/**
* Compilation mode.
*/
enum Mode {
/**
* Insert the breakpoint into the generated compiled code.
*/
BREAK("break"),
/**
* Print the method and it's profile.
*/
PRINT("print"),
/**
* Exclude the method from the compilation.
* 不編譯該方法 —— 用解釋替代。
*/
EXCLUDE("exclude"),
/**
* Force inline.
* 要求編譯器內嵌該方法。
*/
INLINE("inline"),
/**
* Force skip inline.
* 該方法不能被內嵌。用於測量方法調用開銷和評估是否該增加JVM的inline閾值
*/
DONT_INLINE("dontinline"),
/**
* Compile only this method, and nothing else.
* 僅編譯被註解的方法,其他的不編譯。
*/
COMPILE_ONLY("compileonly"),;
}
}
基準測試中建議
編譯器還有其他一些優化行爲,常見的包括(更多參見):死代碼消除(DCE)、方法內斂(method inline)、循環優化、常量摺疊。
方法內斂可以用上面提到的註解做控制,但更多的需要我們在些測試時採用一些小技巧規避,比如:DCE可以根據情況採用Blackhole消費輸出結果。
-
測試前後較重的處理放在
@Setup
和@TearDown
中 -
注意DCE(死代碼消除)
-
不出現循環被編譯器優化,具體建議參考JMHSample_34_SafeLooping
-
測試必須爲fork,fork是分離出來子進程進行測試,
@fork(2)
含義爲順次(one-by-one)fork出子進程來測試 -
使用
@fork
多次fork測試,減少運行間差異 -
多線程測試時參考JMHSample_17_SyncIterations
-
對非循環方法需要測量冷啓動的時間消耗,參考JMHSample_26_BatchSize
-
可以通過profiler獲得基準測試時JVM的相關信息,比如棧、gc、classloader。參考JMHSample_35_Profilers
-
在使用profiler時遇到
no tty present and no askpass program specified
錯誤是因爲帳號並沒有開啓sudo免密導致的。
通過以下步驟可解決,但測試完成後安全起見建議刪除:sudo visudo;在文件最後追加 userName ALL=(ALL) NOPASSWD:ALL
知識點記錄
編譯優化
常見的編譯器優化包括(更多參見):死代碼消除(DCE)、方法內斂(method inline)、循環優化、常量摺疊。
方法內斂
許多優化手段都試圖消除機器級跳轉指令(例如,x86架構的JMP指令)。跳轉指令會修改指令指針寄存器,因此而改變了執行流程。
相比於其他彙編指令,跳轉指令是一個代價高昂的指令,這也是爲什麼大多數優化手段會試圖減少甚至是消除跳轉指令。
內聯是一種家喻戶曉而且好評如潮的優化手段,這是因爲跳轉指令代價高昂,而內聯技術可以將經常調用的、具有不容入口地址的小方法整合到調用方法中。
Listing 3到Listing 5中的Java代碼展示了使用內聯的用法。
Listing 3. Caller method
int whenToEvaluateZing(int y) {
return daysLeft(y) + daysLeft(0) + daysLeft(y+1);
}
Listing 4. Called method
int daysLeft(int x){
if (x == 0)
return 0;
else
return x - 1;
}
Listing 5. Inlined method
int whenToEvaluateZing(int y){
int temp = 0;
if(y == 0) temp += 0; else temp += y - 1;
if(0 == 0) temp += 0; else temp += 0 - 1;
if(y+1 == 0) temp += 0; else temp += (y + 1) - 1;
return temp;
}
在Listing 3到Listing 5的代碼中,展示了將調用3次小方法進行內聯的示例,這裏我們認爲使用內聯比跳轉有更多的優勢。
如果被內聯的方法本身就很少被調用的話,那麼使用內聯也沒什麼意義,但是對頻繁調用的“熱點”方法進行內聯在性能上會有很大的提升。
此外,經過內聯處理後,就可以對內聯後的代碼進行進一步的優化,正如Listing 6中所展示的那樣。
Listing 6. After inlining, more optimizations can be applied
int whenToEvaluateZing(int y){
if(y == 0) return y;
else if (y == -1) return y - 1;
else return y + y - 1;
}
unrolled loop 循環展開
for (i = 1; i <= 60; i++)
a[i] = a[i] * b + c;
可以如此循環展開:
for (i = 1; i <= 20; i+=3)
{
a[i] = a[i] * b + c;
a[i+1] = a[i+1] * b + c;
a[i+2] = a[i+2] * b + c;
}
分支預測
底層對循環中判斷的優化。簡單理解,對執行的循環判斷做採樣,根據採樣來預測下一個判斷會走到哪個分支中。
參考這篇文章---->深入理解CPU的分支預測(Branch Prediction)模型<----做進一步瞭解。