?JMH,即Java Microbenchmark Harness,這是專門用于進(jìn)行代碼的微基準(zhǔn)測試的一套工具API。
?JMH 是一個(gè)由 OpenJDK/Oracle 里面那群開發(fā)了 Java 編譯器的大牛們所開發(fā)的 Micro Benchmark Framework 。何謂 Micro Benchmark 呢?簡單地說就是在 method 層面上的 benchmark,精度可以精確到微秒級(jí)??梢钥闯?JMH 主要使用在當(dāng)你已經(jīng)找出了熱點(diǎn)函數(shù),而需要對(duì)熱點(diǎn)函數(shù)進(jìn)行進(jìn)一步的優(yōu)化時(shí),就可以使用 JMH 對(duì)優(yōu)化的效果進(jìn)行定量的分析。
?比較典型的使用場景還有:
- 當(dāng)你已經(jīng)找出了熱點(diǎn)函數(shù),而需要對(duì)熱點(diǎn)函數(shù)進(jìn)行進(jìn)一步的優(yōu)化時(shí),就可以使用 JMH 對(duì)優(yōu)化的效果進(jìn)行定量的分析。
- 想定量地知道某個(gè)函數(shù)需要執(zhí)行多長時(shí)間,以及執(zhí)行時(shí)間和輸入 n 的相關(guān)性
- 一個(gè)函數(shù)有兩種不同實(shí)現(xiàn)(例如實(shí)現(xiàn) A 使用了 FixedThreadPool,實(shí)現(xiàn) B 使用了 ForkJoinPool),不知道哪種實(shí)現(xiàn)性能更好
?盡管 JMH 是一個(gè)相當(dāng)不錯(cuò)的 Micro Benchmark Framework,但很無奈的是網(wǎng)上能夠找到的文檔比較少,而官方也沒有提供比較詳細(xì)的文檔,對(duì)使用造成了一定的障礙。但是有個(gè)好消息是官方的 Code Sample 寫得非常淺顯易懂,推薦在需要詳細(xì)了解 JMH 的用法時(shí)可以通讀一遍——本文則會(huì)介紹 JMH 最典型的用法和部分常用選項(xiàng)。
第一個(gè)例子
如果你使用 maven 來管理你的 Java 項(xiàng)目的話,引入 JMH 是一件很簡單的事情——只需要在 pom.xml 里增加 JMH 的依賴即可
<properties>
<jmh.version>1.21</jmh.version>
</properties>
<dependencies>
<!-- JMH-->
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>${jmh.version}</version>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>${jmh.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
?接下來我寫一個(gè)比較字符串連接操作的時(shí)候,直接使用字符串相加和使用StringBuilder的append方式的性能比較測試:
/**
* <Description> 比較字符串直接相加和StringBuilder的效率<br>
*
* @author Sunny<br>
* @version 1.0<br>
* @taskId: <br>
* @createDate 2018/12/04 23:13 <br>
* @see com.sunny.jmh.string <br>
*/
@BenchmarkMode(Mode.Throughput)
@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 10, time = 5, timeUnit = TimeUnit.SECONDS)
@Threads(8)
@Fork(2)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
public class StringBuilderBenchmark {
@Benchmark
public void testStringAdd() {
String a = "";
for (int i = 0; i < 10; i++) {
a += i;
}
print(a);
}
@Benchmark
public void testStringBuilderAdd() {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10; i++) {
sb.append(i);
}
print(sb.toString());
}
private void print(String a) {
}
}
?這個(gè)代碼里面有好多注解,你第一次見可能不知道什么意思。先不用管,我待會(huì)一一介紹。
?我們來運(yùn)行這個(gè)測試,運(yùn)行JMH基準(zhǔn)測試有多種方式,一個(gè)是生成jar文件執(zhí)行, 一個(gè)是直接寫main函數(shù)或?qū)憜卧獪y試執(zhí)行。
?一般對(duì)于大型的測試,需要測試時(shí)間比較久,線程比較多的話,就需要去寫好了丟到linux程序里執(zhí)行, 不然本機(jī)執(zhí)行很久時(shí)間什么都干不了了。
mvn clean package
java -jar target/benchmarks.jar
?先編譯打包之后,然后執(zhí)行就可以了。當(dāng)然在執(zhí)行的時(shí)候可以輸入-h參數(shù)來看幫助。
?另外如果對(duì)于一些小的測試,比如我寫的上面這個(gè)小例子,在IDE里面就可以完成了,丟到linux上去太麻煩。 這時(shí)候可以在里面添加一個(gè)main函數(shù)如下:
public static void main(String[] args) throws RunnerException {
Options options = new OptionsBuilder()
.include(StringBuilderBenchmark.class.getSimpleName())
.output("E:/Benchmark.log")
.build();
new Runner(options).run();
}
?這里其實(shí)也比較簡單,new個(gè)Options,然后傳入要運(yùn)行哪個(gè)測試,選擇基準(zhǔn)測試報(bào)告輸出文件地址,然后通過Runner的run方法就可以跑起來了。
?測試報(bào)告如下:
# JMH version: 1.21
# VM version: JDK 1.8.0_144, Java HotSpot(TM) 64-Bit Server VM, 25.144-b01
# VM invoker: C:\ProgramFiles\Java\jdk1.8.0_144\jre\bin\java.exe
# VM options: -Dvisualvm.id=173373474307600 -javaagent:C:\ProgramFiles\JetBrains\IntelliJ IDEA 2017.2.1\lib\idea_rt.jar=57482:C:\ProgramFiles\JetBrains\IntelliJ IDEA 2017.2.1\bin -Dfile.encoding=UTF-8
# Warmup: 5 iterations, 1 s each
# Measurement: 10 iterations, 5 s each
# Timeout: 10 min per iteration
# Threads: 8 threads, will synchronize iterations
# Benchmark mode: Throughput, ops/time
# Benchmark: com.sunny.jmh.demo.StringBuilderBenchmark.testStringAdd
# Run progress: 0.00% complete, ETA 00:07:20
# Fork: 1 of 2
# Warmup Iteration 1: 6070.405 ops/ms
# Warmup Iteration 2: 8255.261 ops/ms
# Warmup Iteration 3: 8698.859 ops/ms
# Warmup Iteration 4: 9141.179 ops/ms
# Warmup Iteration 5: 8182.672 ops/ms
Iteration 1: 8013.397 ops/ms
Iteration 2: 7866.167 ops/ms
Iteration 3: 7928.024 ops/ms
Iteration 4: 8080.173 ops/ms
Iteration 5: 8073.074 ops/ms
Iteration 6: 8495.461 ops/ms
Iteration 7: 7709.082 ops/ms
Iteration 8: 8329.249 ops/ms
Iteration 9: 8112.167 ops/ms
Iteration 10: 7823.246 ops/ms
# Run progress: 12.50% complete, ETA 00:07:10
# Fork: 2 of 2
# Warmup Iteration 1: 5506.481 ops/ms
# Warmup Iteration 2: 7616.767 ops/ms
# Warmup Iteration 3: 7893.031 ops/ms
# Warmup Iteration 4: 7581.665 ops/ms
# Warmup Iteration 5: 7622.357 ops/ms
Iteration 1: 7999.950 ops/ms
Iteration 2: 7947.046 ops/ms
Iteration 3: 7791.413 ops/ms
Iteration 4: 8263.884 ops/ms
Iteration 5: 8083.529 ops/ms
Iteration 6: 8429.626 ops/ms
Iteration 7: 7999.973 ops/ms
Iteration 8: 8267.097 ops/ms
Iteration 9: 8354.462 ops/ms
Iteration 10: 8320.870 ops/ms
Result "com.sunny.jmh.demo.StringBuilderBenchmark.testStringAdd":
8094.394 ±(99.9%) 193.825 ops/ms [Average]
(min, avg, max) = (7709.082, 8094.394, 8495.461), stdev = 223.209
CI (99.9%): [7900.570, 8288.219] (assumes normal distribution)
# JMH version: 1.21
# VM version: JDK 1.8.0_144, Java HotSpot(TM) 64-Bit Server VM, 25.144-b01
# VM invoker: C:\ProgramFiles\Java\jdk1.8.0_144\jre\bin\java.exe
# VM options: -Dvisualvm.id=173373474307600 -javaagent:C:\ProgramFiles\JetBrains\IntelliJ IDEA 2017.2.1\lib\idea_rt.jar=57482:C:\ProgramFiles\JetBrains\IntelliJ IDEA 2017.2.1\bin -Dfile.encoding=UTF-8
# Warmup: 5 iterations, 1 s each
# Measurement: 10 iterations, 5 s each
# Timeout: 10 min per iteration
# Threads: 8 threads, will synchronize iterations
# Benchmark mode: Throughput, ops/time
# Benchmark: com.sunny.jmh.demo.StringBuilderBenchmark.testStringBuilderAdd
# Run progress: 25.00% complete, ETA 00:06:06
# Fork: 1 of 2
# Warmup Iteration 1: 20955.985 ops/ms
# Warmup Iteration 2: 26262.316 ops/ms
# Warmup Iteration 3: 20401.440 ops/ms
# Warmup Iteration 4: 19642.776 ops/ms
# Warmup Iteration 5: 21810.749 ops/ms
Iteration 1: 20276.237 ops/ms
Iteration 2: 21604.222 ops/ms
Iteration 3: 21094.282 ops/ms
Iteration 4: 22015.407 ops/ms
Iteration 5: 20207.019 ops/ms
Iteration 6: 21496.461 ops/ms
Iteration 7: 21413.303 ops/ms
Iteration 8: 20926.099 ops/ms
Iteration 9: 21938.762 ops/ms
Iteration 10: 20640.369 ops/ms
# Run progress: 37.50% complete, ETA 00:05:03
# Fork: 2 of 2
# Warmup Iteration 1: 25752.391 ops/ms
# Warmup Iteration 2: 33634.702 ops/ms
# Warmup Iteration 3: 19614.646 ops/ms
# Warmup Iteration 4: 21379.381 ops/ms
# Warmup Iteration 5: 20710.261 ops/ms
Iteration 1: 20279.798 ops/ms
Iteration 2: 21302.430 ops/ms
Iteration 3: 20715.865 ops/ms
Iteration 4: 22068.016 ops/ms
Iteration 5: 21006.399 ops/ms
Iteration 6: 20509.550 ops/ms
Iteration 7: 21605.828 ops/ms
Iteration 8: 20125.785 ops/ms
Iteration 9: 21470.167 ops/ms
Iteration 10: 20688.491 ops/ms
Result "com.sunny.jmh.demo.StringBuilderBenchmark.testStringBuilderAdd":
21069.224 ±(99.9%) 542.533 ops/ms [Average]
(min, avg, max) = (20125.785, 21069.224, 22068.016), stdev = 624.782
CI (99.9%): [20526.691, 21611.758] (assumes normal distribution)
# JMH version: 1.21
# VM version: JDK 1.8.0_144, Java HotSpot(TM) 64-Bit Server VM, 25.144-b01
# VM invoker: C:\ProgramFiles\Java\jdk1.8.0_144\jre\bin\java.exe
# VM options: -Dvisualvm.id=173373474307600 -javaagent:C:\ProgramFiles\JetBrains\IntelliJ IDEA 2017.2.1\lib\idea_rt.jar=57482:C:\ProgramFiles\JetBrains\IntelliJ IDEA 2017.2.1\bin -Dfile.encoding=UTF-8
# Warmup: 5 iterations, 1 s each
# Measurement: 10 iterations, 5 s each
# Timeout: 10 min per iteration
# Threads: 8 threads, will synchronize iterations
# Benchmark mode: Throughput, ops/time
# Benchmark: com.sunny.jmh.string.StringBuilderBenchmark.testStringAdd
# Run progress: 50.00% complete, ETA 00:04:01
# Fork: 1 of 2
# Warmup Iteration 1: 7091.196 ops/ms
# Warmup Iteration 2: 8846.815 ops/ms
# Warmup Iteration 3: 8191.191 ops/ms
# Warmup Iteration 4: 7918.475 ops/ms
# Warmup Iteration 5: 7677.783 ops/ms
Iteration 1: 8107.558 ops/ms
Iteration 2: 7804.800 ops/ms
Iteration 3: 7815.767 ops/ms
Iteration 4: 8445.645 ops/ms
Iteration 5: 8074.722 ops/ms
Iteration 6: 8218.089 ops/ms
Iteration 7: 8163.977 ops/ms
Iteration 8: 7843.616 ops/ms
Iteration 9: 8557.757 ops/ms
Iteration 10: 8025.778 ops/ms
# Run progress: 62.50% complete, ETA 00:03:01
# Fork: 2 of 2
# Warmup Iteration 1: 6632.392 ops/ms
# Warmup Iteration 2: 7942.598 ops/ms
# Warmup Iteration 3: 7741.897 ops/ms
# Warmup Iteration 4: 8969.824 ops/ms
# Warmup Iteration 5: 9159.238 ops/ms
Iteration 1: 9592.754 ops/ms
Iteration 2: 9136.667 ops/ms
Iteration 3: 7361.047 ops/ms
Iteration 4: 9217.467 ops/ms
Iteration 5: 9220.959 ops/ms
Iteration 6: 9740.287 ops/ms
Iteration 7: 9589.307 ops/ms
Iteration 8: 9415.877 ops/ms
Iteration 9: 9015.438 ops/ms
Iteration 10: 8012.396 ops/ms
Result "com.sunny.jmh.string.StringBuilderBenchmark.testStringAdd":
8567.995 ±(99.9%) 631.440 ops/ms [Average]
(min, avg, max) = (7361.047, 8567.995, 9740.287), stdev = 727.167
CI (99.9%): [7936.556, 9199.435] (assumes normal distribution)
# JMH version: 1.21
# VM version: JDK 1.8.0_144, Java HotSpot(TM) 64-Bit Server VM, 25.144-b01
# VM invoker: C:\ProgramFiles\Java\jdk1.8.0_144\jre\bin\java.exe
# VM options: -Dvisualvm.id=173373474307600 -javaagent:C:\ProgramFiles\JetBrains\IntelliJ IDEA 2017.2.1\lib\idea_rt.jar=57482:C:\ProgramFiles\JetBrains\IntelliJ IDEA 2017.2.1\bin -Dfile.encoding=UTF-8
# Warmup: 5 iterations, 1 s each
# Measurement: 10 iterations, 5 s each
# Timeout: 10 min per iteration
# Threads: 8 threads, will synchronize iterations
# Benchmark mode: Throughput, ops/time
# Benchmark: com.sunny.jmh.string.StringBuilderBenchmark.testStringBuilderAdd
# Run progress: 75.00% complete, ETA 00:02:00
# Fork: 1 of 2
# Warmup Iteration 1: 29514.594 ops/ms
# Warmup Iteration 2: 34363.625 ops/ms
# Warmup Iteration 3: 21830.529 ops/ms
# Warmup Iteration 4: 16672.395 ops/ms
# Warmup Iteration 5: 19167.425 ops/ms
Iteration 1: 23537.009 ops/ms
Iteration 2: 26558.898 ops/ms
Iteration 3: 28540.710 ops/ms
Iteration 4: 28857.648 ops/ms
Iteration 5: 26367.646 ops/ms
Iteration 6: 28499.358 ops/ms
Iteration 7: 28387.032 ops/ms
Iteration 8: 27544.734 ops/ms
Iteration 9: 28621.245 ops/ms
Iteration 10: 27376.623 ops/ms
# Run progress: 87.50% complete, ETA 00:01:00
# Fork: 2 of 2
# Warmup Iteration 1: 31498.127 ops/ms
# Warmup Iteration 2: 26523.053 ops/ms
# Warmup Iteration 3: 24347.554 ops/ms
# Warmup Iteration 4: 25594.657 ops/ms
# Warmup Iteration 5: 24913.044 ops/ms
Iteration 1: 25242.148 ops/ms
Iteration 2: 24921.964 ops/ms
Iteration 3: 25271.209 ops/ms
Iteration 4: 23288.893 ops/ms
Iteration 5: 23138.775 ops/ms
Iteration 6: 25735.081 ops/ms
Iteration 7: 25004.206 ops/ms
Iteration 8: 24597.182 ops/ms
Iteration 9: 24222.304 ops/ms
Iteration 10: 19132.539 ops/ms
Result "com.sunny.jmh.string.StringBuilderBenchmark.testStringBuilderAdd":
25742.260 ±(99.9%) 2127.587 ops/ms [Average]
(min, avg, max) = (19132.539, 25742.260, 28857.648), stdev = 2450.132
CI (99.9%): [23614.673, 27869.847] (assumes normal distribution)
# Run complete. Total time: 00:08:03
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 Mode Cnt Score Error Units
c.s.j.demo.StringBuilderBenchmark.testStringAdd thrpt 20 8094.394 ± 193.825 ops/ms
c.s.j.demo.StringBuilderBenchmark.testStringBuilderAdd thrpt 20 21069.224 ± 542.533 ops/ms
c.s.j.string.StringBuilderBenchmark.testStringAdd thrpt 20 8567.995 ± 631.440 ops/ms
c.s.j.string.StringBuilderBenchmark.testStringBuilderAdd thrpt 20 25742.260 ± 2127.587 ops/ms
?仔細(xì)看,三大部分,第一部分是字符串用加號(hào)連接執(zhí)行的結(jié)果,第二部分是StringBuilder執(zhí)行的結(jié)果,第三部分就是兩個(gè)的簡單結(jié)果比較。這里注意我們forks傳的2,所以每個(gè)測試有兩個(gè)fork結(jié)果。
?前兩部分是一樣的,簡單說下。首先會(huì)寫出每部分的一些參數(shù)設(shè)置,然后是預(yù)熱迭代執(zhí)行(Warmup Iteration), 然后是正常的迭代執(zhí)行(Iteration),最后是結(jié)果(Result)。這些看看就好,我們最關(guān)注的就是第三部分, 其實(shí)也就是最終的結(jié)論。千萬別看歪了,他輸出的也確實(shí)很不爽,error那列其實(shí)沒有內(nèi)容,score(thrpt)的結(jié)果是xxx ± xxx,單位是每毫秒多少個(gè)操作??梢钥吹剑琒tringBuilder的速度還確實(shí)是要比String進(jìn)行文字疊加的效率好太多。
注解介紹
好了,當(dāng)你對(duì)JMH有了一個(gè)基本認(rèn)識(shí)后,現(xiàn)在來詳細(xì)解釋一下前面代碼中的各個(gè)注解含義。
@BenchmarkMode
基準(zhǔn)測試類型。這里選擇的是Throughput也就是吞吐量。根據(jù)源碼點(diǎn)進(jìn)去,每種類型后面都有對(duì)應(yīng)的解釋,比較好理解,吞吐量會(huì)得到單位時(shí)間內(nèi)可以進(jìn)行的操作數(shù)。
- Throughput: 整體吞吐量,例如“1秒內(nèi)可以執(zhí)行多少次調(diào)用”。
- AverageTime: 調(diào)用的平均時(shí)間,例如“每次調(diào)用平均耗時(shí)xxx毫秒”。
- SampleTime: 隨機(jī)取樣,最后輸出取樣結(jié)果的分布,例如“99%的調(diào)用在xxx毫秒以內(nèi),99.99%的調(diào)用在xxx毫秒以內(nèi)”
- SingleShotTime: 以上模式都是默認(rèn)一次 iteration 是 1s,唯有 SingleShotTime 是只運(yùn)行一次。往往同時(shí)把 warmup 次數(shù)設(shè)為0,用于測試?yán)鋯?dòng)時(shí)的性能。
- All(“all”, “All benchmark modes”);
@Warmup
上面我們提到了,進(jìn)行基準(zhǔn)測試前需要進(jìn)行預(yù)熱。一般我們前幾次進(jìn)行程序測試的時(shí)候都會(huì)比較慢, 所以要讓程序進(jìn)行幾輪預(yù)熱,保證測試的準(zhǔn)確性。其中的參數(shù)iterations也就非常好理解了,就是預(yù)熱輪數(shù)。
為什么需要預(yù)熱?因?yàn)?JVM 的 JIT 機(jī)制的存在,如果某個(gè)函數(shù)被調(diào)用多次之后,JVM 會(huì)嘗試將其編譯成為機(jī)器碼從而提高執(zhí)行速度。所以為了讓 benchmark 的結(jié)果更加接近真實(shí)情況就需要進(jìn)行預(yù)熱。
- iterations:預(yù)熱的次數(shù)。
- time:每次預(yù)熱的時(shí)間。
- timeUnit:時(shí)間的單位,默認(rèn)秒。
- batchSize:批處理大小,每次操作調(diào)用幾次方法。
@Measurement
度量,其實(shí)就是一些基本的測試參數(shù)。
- iterations 進(jìn)行測試的輪次
- time 每輪進(jìn)行的時(shí)長
- timeUnit 時(shí)長單位
都是一些基本的參數(shù),可以根據(jù)具體情況調(diào)整。一般比較重的東西可以進(jìn)行大量的測試,放到服務(wù)器上運(yùn)行。
@Threads
每個(gè)進(jìn)程中的測試線程,可用于類或者方法上。一般選擇為cpu乘以2。如果配置了 Threads.MAX ,代表使用 Runtime.getRuntime().availableProcessors() 個(gè)線程。
@Fork
進(jìn)行 fork 的次數(shù)??捎糜陬惢蛘叻椒ㄉ?。如果 fork 數(shù)是2的話,則 JMH 會(huì) fork 出兩個(gè)進(jìn)程來進(jìn)行測試。
@OutputTimeUnit
這個(gè)比較簡單了,基準(zhǔn)測試結(jié)果的時(shí)間類型。一般選擇秒、毫秒、微秒。
@Benchmark
方法級(jí)注解,表示該方法是需要進(jìn)行 benchmark 的對(duì)象,用法和 JUnit 的 @Test 類似。
@Param
屬性級(jí)注解,@Param 可以用來指定某項(xiàng)參數(shù)的多種情況。特別適合用來測試一個(gè)函數(shù)在不同的參數(shù)輸入的情況下的性能。
@Setup
方法級(jí)注解,這個(gè)注解的作用就是我們需要在測試之前進(jìn)行一些準(zhǔn)備工作,比如對(duì)一些數(shù)據(jù)的初始化之類的。
@TearDown
方法級(jí)注解,這個(gè)注解的作用就是我們需要在測試之后進(jìn)行一些結(jié)束工作,比如關(guān)閉線程池,數(shù)據(jù)庫連接等的,主要用于資源的回收等。
@Setup主要實(shí)現(xiàn)測試前的初始化工作,只能作用在方法上。用法和Junit一樣。使用該注解必須定義 @State注解。
@TearDown主要實(shí)現(xiàn)測試完成后的垃圾回收等工作,只能作用在方法上。用法和Junit一樣。使用該注解必須定義 @State 注解。
這兩個(gè)注解都有一個(gè) Level 的枚舉value,它有三個(gè)值(默認(rèn)的是Trial):
- Trial:在每次Benchmark的之前/之后執(zhí)行。
- Iteration:在每次Benchmark的iteration的之前/之后執(zhí)行。
- Invocation:每次調(diào)用Benchmark標(biāo)記的方法之前/之后都會(huì)執(zhí)行。
可見,Level的粒度從Trial到Invocation越來越細(xì)。
@State
當(dāng)使用@Setup參數(shù)的時(shí)候,必須在類上加這個(gè)參數(shù),不然會(huì)提示無法運(yùn)行。
State 用于聲明某個(gè)類是一個(gè)“狀態(tài)”,然后接受一個(gè) Scope 參數(shù)用來表示該狀態(tài)的共享范圍。 因?yàn)楹芏?benchmark 會(huì)需要一些表示狀態(tài)的類,JMH 允許你把這些類以依賴注入的方式注入到 benchmark 函數(shù)里。Scope 主要分為三種。
- Thread: 該狀態(tài)為每個(gè)線程獨(dú)享。
- Group: 該狀態(tài)為同一個(gè)組里面所有線程共享。
- Benchmark: 該狀態(tài)在所有線程間共享。
首先說一下Benchmark,對(duì)于同一個(gè)@Benchmark,所有線程共享實(shí)例,也就是只會(huì)new Person 1次
@State(Scope.Benchmark)
public static class BenchmarkState {
Person person = new Person(21, "ben", "benchmark");
volatile double x = Math.PI;
}
@Benchmark
public void measureShared(BenchmarkState state) {
state.x++;
}
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(JMHSample_03_States.class.getSimpleName())
.threads(8)
.warmupTime(TimeValue.seconds(1))
.measurementTime(TimeValue.seconds(1))
.forks(1)
.build();
new Runner(opt).run();
}
再說一下thread,這個(gè)比較好理解,不同線程之間的實(shí)例不共享。對(duì)于上面我們?cè)O(shè)定的線程數(shù)為8個(gè),也就是會(huì)new Person 8次。
@State(Scope.Thread)
public static class ThreadState {
Person person = new Person(21, "ben", "thread");
volatile double x = Math.PI;
}
@Benchmark
public void measureUnshared(ThreadState state) {
state.x++;
}
而對(duì)于Group來說,同一個(gè)group的作為一個(gè)執(zhí)行單元,所以 measureGroup 和 measureGroup2 共享8個(gè)線程,所以一個(gè)方法也就會(huì)執(zhí)行new Person 4次。
@State(Scope.Group)
public static class GroupState {
Person person = new Person(21, "ben", "group");
volatile double x = Math.PI;
}
@Benchmark
@Group("ben")
public void measureGroup(GroupState state) {
state.x++;
}
@Benchmark
@Group("ben")
public void measureGroup2(GroupState state) {
state.x++;
}
關(guān)于State的用法,官方的 code sample 里有比較好的例子。
@Group
結(jié)合@Benchmark一起使用,把多個(gè)基準(zhǔn)方法歸為一類,只能作用在方法上。同一個(gè)組中的所有測試設(shè)置相同的名稱(否則這些測試將獨(dú)立運(yùn)行——沒有任何警告提示!)
@GroupThreads
定義了多少個(gè)線程參與在組中運(yùn)行基準(zhǔn)方法。只能作用在方法上。
@OutputTimeUnit
這個(gè)比較簡單了,基準(zhǔn)測試結(jié)果的時(shí)間類型。可用于類或者方法上。一般選擇秒、毫秒、微秒。
@CompilerControl
該注解可以控制方法編譯的行為,可用于類或者方法或者構(gòu)造函數(shù)上。它內(nèi)部有6種模式,這里我們只關(guān)心三種重要的模式:
- CompilerControl.Mode.INLINE:強(qiáng)制使用內(nèi)聯(lián)。
- CompilerControl.Mode.DONT_INLINE:禁止使用內(nèi)聯(lián)。
- CompilerControl.Mode.EXCLUDE:禁止編譯方法。
public void target_blank() {
}
@CompilerControl(CompilerControl.Mode.DONT_INLINE)
public void target_dontInline() {
}
@CompilerControl(CompilerControl.Mode.INLINE)
public void target_inline() {
}
@CompilerControl(CompilerControl.Mode.EXCLUDE)
public void target_exclude() {
}
@Benchmark
public void baseline() {
}
@Benchmark
public void blank() {
target_blank();
}
@Benchmark
public void dontinline() {
target_dontInline();
}
@Benchmark
public void inline() {
target_inline();
}
@Benchmark
public void exclude() {
target_exclude();
}
最后得出的結(jié)果也表名,使用內(nèi)聯(lián)優(yōu)化會(huì)影響實(shí)際的結(jié)果:
Benchmark Mode Cnt Score Error Units
JMHSample_16_CompilerControl.baseline avgt 3 0.338 ± 0.475 ns/op
JMHSample_16_CompilerControl.blank avgt 3 0.343 ± 0.213 ns/op
JMHSample_16_CompilerControl.dontinline avgt 3 2.247 ± 0.421 ns/op
JMHSample_16_CompilerControl.exclude avgt 3 82.814 ± 7.333 ns/op
JMHSample_16_CompilerControl.inline avgt 3 0.322 ± 0.023 ns/op
避免JIT優(yōu)化
我們?cè)跍y試的時(shí)候,一定要避免JIT優(yōu)化。對(duì)于有一些代碼,編譯器可以推導(dǎo)出一些計(jì)算是多余的,并且完全消除它們。 如果我們的基準(zhǔn)測試?yán)镉胁糠执a被清除了,那測試的結(jié)果就不準(zhǔn)確了。比如下面這一段代碼:
private double x = Math.PI;
@Benchmark
public void baseline() {
// do nothing, this is a baseline
}
@Benchmark
public void measureWrong() {
// This is wrong: result is not used and the entire computation is optimized away.
Math.log(x);
}
@Benchmark
public double measureRight() {
// This is correct: the result is being used.
return Math.log(x);
}
由于 measureWrong 方法被編譯器優(yōu)化了,導(dǎo)致效果和 baseline 方法一樣變成了空方法,結(jié)果也證實(shí)了這一點(diǎn):
Benchmark Mode Cnt Score Error Units
JMHSample_08_DeadCode.baseline avgt 5 0.311 ± 0.018 ns/op
JMHSample_08_DeadCode.measureRight avgt 5 23.702 ± 0.320 ns/op
JMHSample_08_DeadCode.measureWrong avgt 5 0.306 ± 0.003 ns/op
如果我們想方法返回值還是void,但是需要讓Math.log(x)的耗時(shí)加入到基準(zhǔn)運(yùn)算中,我們可以使用JMH提供給我們的類 Blackhole ,使用它的 consume 來避免JIT的優(yōu)化消除。
@Benchmark
public void measureRight_2(Blackhole bh) {
bh.consume(Math.log(x));
}
但是有返回值的方法就不會(huì)被優(yōu)化了嗎?你想的太多了。。。重新改改剛才的代碼,讓字段 x 變成final的。
private final double x = Math.PI;
運(yùn)行后的結(jié)果發(fā)現(xiàn) measureRight 被JIT進(jìn)行了優(yōu)化,從 23.7ns/op 降到了 2.5ns/op
JMHSample_08_DeadCode.measureRight avgt 5 2.587 ± 0.081 ns/op
當(dāng)然 Math.log(Math.PI ); 這種返回寫法和字段定義成final一樣,都會(huì)被進(jìn)行優(yōu)化。
優(yōu)化的原因是因?yàn)镴VM認(rèn)為每次計(jì)算的結(jié)果都是相同的,于是就會(huì)把相同代碼移到了JMH的循環(huán)之外。
結(jié)論:
- 基準(zhǔn)測試方法一定不要返回void。
- 如果要使用void返回,可以使用
Blackhole的consume來避免JIT的優(yōu)化消除。 - 計(jì)算不要引用常量,否則會(huì)被優(yōu)化到JMH的循環(huán)之外。
第二個(gè)例子
在看過第一個(gè)完全只為示范的例子之后,再來看一個(gè)有實(shí)際意義的例子。
問題:
計(jì)算 1 ~ n 之和,比較串行算法和并行算法的效率,看 n 在大約多少時(shí)并行算法開始超越串行算法
首先定義一個(gè)表示這兩種實(shí)現(xiàn)的接口
public interface Calculator {
/**
* calculate sum of an integer array
* @param numbers
* @return
*/
public long sum(int[] numbers);
/**
* shutdown pool or reclaim any related resources
*/
public void shutdown();
}
由于這兩種算法的實(shí)現(xiàn)不是這篇文章的重點(diǎn),而且本身并不困難,所以實(shí)際代碼就不贅述了。如果真的感興趣的話,可以看最后的附錄。以下僅說明一下我所指的串行算法和并行算法的含義。
串行算法:使用 for-loop 來計(jì)算 n 個(gè)正整數(shù)之和。
并行算法:將所需要計(jì)算的 n 個(gè)正整數(shù)分成 m 份,交給 m 個(gè)線程分別計(jì)算出和以后,再把它們的結(jié)果相加。
進(jìn)行 benchmark 的代碼如下
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@State(Scope.Benchmark)
public class SecondBenchmark {
@Param({"10000", "100000", "1000000"})
private int length;
private int[] numbers;
private Calculator singleThreadCalc;
private Calculator multiThreadCalc;
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(SecondBenchmark.class.getSimpleName())
.forks(2)
.warmupIterations(5)
.measurementIterations(5)
.build();
new Runner(opt).run();
}
@Benchmark
public long singleThreadBench() {
return singleThreadCalc.sum(numbers);
}
@Benchmark
public long multiThreadBench() {
return multiThreadCalc.sum(numbers);
}
@Setup
public void prepare() {
numbers = IntStream.rangeClosed(1, length).toArray();
singleThreadCalc = new SinglethreadCalculator();
multiThreadCalc = new MultithreadCalculator(Runtime.getRuntime().availableProcessors());
}
@TearDown
public void shutdown() {
singleThreadCalc.shutdown();
multiThreadCalc.shutdown();
}
}
注意到這里用到了3個(gè)之前沒有使用的注解。
一些值得注意的地方
無用代碼消除(Dead Code Elimination)
現(xiàn)代編譯器是十分聰明的,它們會(huì)對(duì)你的代碼進(jìn)行推導(dǎo)分析,判定哪些代碼是無用的然后進(jìn)行去除,這種行為對(duì)微基準(zhǔn)測試是致命的,它會(huì)使你無法準(zhǔn)確測試出你的方法性能。JMH本身已經(jīng)對(duì)這種情況做了處理,你只要記?。?.永遠(yuǎn)不要寫void方法;2.在方法結(jié)束返回你的計(jì)算結(jié)果。有時(shí)候如果需要返回多于一個(gè)結(jié)果,可以考慮自行合并計(jì)算結(jié)果,或者使用JMH提供的BlackHole對(duì)象:
/*
* This demonstrates Option A:
*
* Merge multiple results into one and return it.
* This is OK when is computation is relatively heavyweight, and merging
* the results does not offset the results much.
*/
@Benchmark
public double measureRight_1() {
return Math.log(x1) + Math.log(x2);
}
/*
* This demonstrates Option B:
*
* Use explicit Blackhole objects, and sink the values there.
* (Background: Blackhole is just another @State object, bundled with JMH).
*/
@Benchmark
public void measureRight_2(Blackhole bh) {
bh.consume(Math.log(x1));
bh.consume(Math.log(x2));
}
常量折疊(Constant Folding)
常量折疊是一種現(xiàn)代編譯器優(yōu)化策略,例如,i = 320 * 200 * 32,多數(shù)的現(xiàn)代編譯器不會(huì)真的產(chǎn)生兩個(gè)乘法的指令再將結(jié)果儲(chǔ)存下來,取而代之的,他們會(huì)辨識(shí)出語句的結(jié)構(gòu),并在編譯時(shí)期將數(shù)值計(jì)算出來(i = 2,048,000)。
在微基準(zhǔn)測試中,如果你的計(jì)算輸入是可預(yù)測的,也不是一個(gè)@State實(shí)例變量,那么很可能會(huì)被JIT給優(yōu)化掉。對(duì)此,JMH的建議是:1.永遠(yuǎn)從@State實(shí)例中讀取你的方法輸入;2.返回你的計(jì)算結(jié)果;3.或者考慮使用BlackHole對(duì)象;
見如下官方例子:
@State(Scope.Thread)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class JMHSample_10_ConstantFold {
private double x = Math.PI;
private final double wrongX = Math.PI;
@Benchmark
public double baseline() {
// simply return the value, this is a baseline
return Math.PI;
}
@Benchmark
public double measureWrong_1() {
// This is wrong: the source is predictable, and computation is foldable.
return Math.log(Math.PI);
}
@Benchmark
public double measureWrong_2() {
// This is wrong: the source is predictable, and computation is foldable.
return Math.log(wrongX);
}
@Benchmark
public double measureRight() {
// This is correct: the source is not predictable.
return Math.log(x);
}
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(JMHSample_10_ConstantFold.class.getSimpleName())
.warmupIterations(5)
.measurementIterations(5)
.forks(1)
.build();
new Runner(opt).run();
}
}
循環(huán)展開(Loop Unwinding)
循環(huán)展開最常用來降低循環(huán)開銷,為具有多個(gè)功能單元的處理器提供指令級(jí)并行。也有利于指令流水線的調(diào)度。例如:
for (i = 1; i <= 60; i++)
a[i] = a[i] * b + c;
可以展開成:
for (i = 1; i <= 60; i+=3)
{
a[i] = a[i] * b + c;
a[i+1] = a[i+1] * b + c;
a[i+2] = a[i+2] * b + c;
}
由于編譯器可能會(huì)對(duì)你的代碼進(jìn)行循環(huán)展開,因此JMH建議不要在你的測試方法中寫任何循環(huán)。如果確實(shí)需要執(zhí)行循環(huán)計(jì)算,可以結(jié)合@BenchmarkMode(Mode.SingleShotTime)和@Measurement(batchSize = N)來達(dá)到同樣的效果。參考如下例子:
/*
* Suppose we want to measure how much it takes to sum two integers:
*/
int x = 1;
int y = 2;
/*
* This is what you do with JMH.
*/
@Benchmark
@OperationsPerInvocation(100)
public int measureRight() {
return (x + y);
}
還有這個(gè)例子:
@State(Scope.Thread)
@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Fork(3)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class JMHSample_34_SafeLooping {
/*
* JMHSample_11_Loops warns about the dangers of using loops in @Benchmark methods.
* Sometimes, however, one needs to traverse through several elements in a dataset.
* This is hard to do without loops, and therefore we need to devise a scheme for
* safe looping.
*/
/*
* Suppose we want to measure how much it takes to execute work() with different
* arguments. This mimics a frequent use case when multiple instances with the same
* implementation, but different data, is measured.
*/
static final int BASE = 42;
static int work(int x) {
return BASE + x;
}
/*
* Every benchmark requires control. We do a trivial control for our benchmarks
* by checking the benchmark costs are growing linearly with increased task size.
* If it doesn't, then something wrong is happening.
*/
@Param({"1", "10", "100", "1000"})
int size;
int[] xs;
@Setup
public void setup() {
xs = new int[size];
for (int c = 0; c < size; c++) {
xs[c] = c;
}
}
/*
* First, the obviously wrong way: "saving" the result into a local variable would not
* work. A sufficiently smart compiler will inline work(), and figure out only the last
* work() call needs to be evaluated. Indeed, if you run it with varying $size, the score
* will stay the same!
*/
@Benchmark
public int measureWrong_1() {
int acc = 0;
for (int x : xs) {
acc = work(x);
}
return acc;
}
/*
* Second, another wrong way: "accumulating" the result into a local variable. While
* it would force the computation of each work() method, there are software pipelining
* effects in action, that can merge the operations between two otherwise distinct work()
* bodies. This will obliterate the benchmark setup.
*
* In this example, HotSpot does the unrolled loop, merges the $BASE operands into a single
* addition to $acc, and then does a bunch of very tight stores of $x-s. The final performance
* depends on how much of the loop unrolling happened *and* how much data is available to make
* the large strides.
*/
@Benchmark
public int measureWrong_2() {
int acc = 0;
for (int x : xs) {
acc += work(x);
}
return acc;
}
/*
* Now, let's see how to measure these things properly. A very straight-forward way to
* break the merging is to sink each result to Blackhole. This will force runtime to compute
* every work() call in full. (We would normally like to care about several concurrent work()
* computations at once, but the memory effects from Blackhole.consume() prevent those optimization
* on most runtimes).
*/
@Benchmark
public void measureRight_1(Blackhole bh) {
for (int x : xs) {
bh.consume(work(x));
}
}
/*
* DANGEROUS AREA, PLEASE READ THE DESCRIPTION BELOW.
*
* Sometimes, the cost of sinking the value into a Blackhole is dominating the nano-benchmark score.
* In these cases, one may try to do a make-shift "sinker" with non-inlineable method. This trick is
* *very* VM-specific, and can only be used if you are verifying the generated code (that's a good
* strategy when dealing with nano-benchmarks anyway).
*
* You SHOULD NOT use this trick in most cases. Apply only where needed.
*/
@Benchmark
public void measureRight_2() {
for (int x : xs) {
sink(work(x));
}
}
@CompilerControl(CompilerControl.Mode.DONT_INLINE)
public static void sink(int v) {
// IT IS VERY IMPORTANT TO MATCH THE SIGNATURE TO AVOID AUTOBOXING.
// The method intentionally does nothing.
}
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(JMHSample_34_SafeLooping.class.getSimpleName())
.warmupIterations(5)
.measurementIterations(5)
.forks(3)
.build();
new Runner(opt).run();
}
}
參考引用
http://blog.dyngr.com/blog/2016/10/29/introduction-of-jmh/
http://benjaminwhx.com/2018/06/15/%E4%BD%BF%E7%94%A8JMH%E5%81%9A%E5%9F%BA%E5%87%86%E6%B5%8B%E8%AF%95/
https://www.xncoding.com/2018/01/07/java/jmh.html