概述
JMH 是一個由 OpenJDK/Oracle 里面那群開發了 JAVA 編譯器的大牛們所開發的 Micro Benchmark Framework 。何謂 Micro Benchmark 呢?簡單地說就是在 method 層面上的 benchmark,精度可以精確到微秒級。可以看出 JMH 主要使用在當你已經找出了熱點函數,而需要對熱點函數進行進一步的優化時,就可以使用 JMH 對優化的效果進行定量的分析。
比較典型的使用場景還有:
•想定量地知道某個函數需要執行多長時間,以及執行時間和輸入 n 的相關性•一個函數有兩種不同實現(例如實現 A 使用了 FixedThreadPool,實現 B 使用了 ForkJoinPool),不知道哪種實現性能更好
盡管 JMH 是一個相當不錯的 Micro Benchmark Framework,但很無奈的是網上能夠找到的文檔比較少,而官方也沒有提供比較詳細的文檔,對使用造成了一定的障礙。但是有個好消息是官方的 Code Sample 寫得非常淺顯易懂,推薦在需要詳細了解 JMH 的用法時可以通讀一遍——本文則會介紹 JMH 最典型的用法和部分常用選項。
第一個例子
如果你使用 maven 來管理你的 Java 項目的話,引入 JMH 是一件很簡單的事情——只需要在 pom.xml
里增加 JMH 的依賴即可
<properties>
<jmh.version>1.14.1</jmh.version>
</properties>
<dependencies>
<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>
接下來再創建我們的第一個 Benchmark
package com.ckj.base.designPatternes.proxy.DynamicProxy;
import org.openjdk.jmh.annotations.*;
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
import java.util.concurrent.TimeUnit;
/**
* @author c.kj
* @Description
* @Date 2021-03-04
* @Time 21:55
**/
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@State(Scope.Thread)
@Fork(1)
public class CglibProxy {
@Benchmark
@Warmup(iterations = 5, time = 100, timeUnit = TimeUnit.MICROSECONDS)
@Measurement(iterations = 5, time = 100, timeUnit = TimeUnit.MICROSECONDS)
public int measureName() throws InterruptedException {
Thread.sleep(1);
getProxyInstance();
return 0;
}
public Object getProxyInstance() {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(CglibTarget.class);
MethodInterceptor methodInterceptor = new MethodInterceptor() {
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy)
throws Throwable {
System.out.println("intercept start....");
Object o1 = methodProxy.invokeSuper(o, objects);
System.out.println("intercept end....");
return o1;
}
};
enhancer.setCallback(methodInterceptor);
Object o = enhancer.create();
return o;
}
}
有不少你可能是第一次見到的注解,不過不著急,接下來會解釋這些注解的意義。我們先來跑一下這個 benchmark 吧 :)
# JMH 1.14.1 (released 1780 days ago, please consider updating!)
# VM version: JDK 1.8.0_201, VM 25.201-b09
# VM invoker: /Library/Java/JavaVirtualmachines/jdk1.8.0_201.jdk/Contents/Home/jre/bin/java
# VM options: -Dvisualvm.id=94033462347806 -javaagent:/Applications/IntelliJ IDEA.app/Contents/lib/idea_rt.jar=56278:/Applications/IntelliJ IDEA.app/Contents/bin -Dfile.encoding=UTF-8
# Warmup: 5 iterations, 100 us each
# Measurement: 5 iterations, 100 us each
# Timeout: 10 min per iteration
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Average time, time/op
# Benchmark: com.ckj.base.designPatternes.proxy.DynamicProxy.CglibProxy.measureName
# Run progress: 0.00% complete, ETA 00:00:00
# Fork: 1 of 1
# Warmup Iteration 1: 68584.809 us/op
# Warmup Iteration 2: 1312.436 us/op
# Warmup Iteration 3: 1329.796 us/op
# Warmup Iteration 4: 1329.558 us/op
# Warmup Iteration 5: 1296.309 us/op
Iteration 1: 1296.129 us/op
Iteration 2: 1121.620 us/op
Iteration 3: 1603.035 us/op
Iteration 4: 1306.412 us/op
Iteration 5: 1194.444 us/op
Result "measureName":
1304.328 ±(99.9%) 706.764 us/op [Average]
(min, avg, max) = (1121.620, 1304.328, 1603.035), stdev = 183.544
CI (99.9%): [597.564, 2011.092] (assumes normal distribution)
# Run complete. Total time: 00:00:01
Benchmark Mode Cnt Score Error Units
designPatternes.proxy.DynamicProxy.CglibProxy.measureName avgt 5 1304.328 ± 706.764 us/op
Process finished with exit code 0
對 getProxyInstance()
的測試結果顯示執行時間平均約為1304.328微秒。因為我們的測試對象 1304.328正好就是睡眠1000微秒,所以 JMH 顯示的結果可以說很符合我們的預期。
那好,現在我們再來詳細地解釋代碼的意義。不過在這之前,需要先了解一下 JMH 的幾個基本概念。
基本概念
Mode
Mode 表示 JMH 進行 Benchmark 時所使用的模式。通常是測量的維度不同,或是測量的方式不同。目前 JMH 共有四種模式:
•Throughput
: 整體吞吐量,例如“1秒內可以執行多少次調用”。•AverageTime
: 調用的平均時間,例如“每次調用平均耗時xxx毫秒”。•SampleTime
: 隨機取樣,最后輸出取樣結果的分布,例如“99%的調用在xxx毫秒以內,99.99%的調用在xxx毫秒以內”•SingleShotTime
: 以上模式都是默認一次 iteration 是 1s,唯有 SingleShotTime
是只運行一次。往往同時把 warmup 次數設為0,用于測試冷啟動時的性能。
Iteration
Iteration 是 JMH 進行測試的最小單位。在大部分模式下,一次 iteration 代表的是一秒,JMH 會在這一秒內不斷調用需要 benchmark 的方法,然后根據模式對其采樣,計算吞吐量,計算平均執行時間等。
Warmup
Warmup 是指在實際進行 benchmark 前先進行預熱的行為。為什么需要預熱?因為 JVM 的 JIT 機制的存在,如果某個函數被調用多次之后,JVM 會嘗試將其編譯成為機器碼從而提高執行速度。所以為了讓 benchmark 的結果更加接近真實情況就需要進行預熱。
注解
現在來解釋一下上面例子中使用到的注解,其實很多注解的意義完全可以望文生義 :)
@Benchmark
表示該方法是需要進行 benchmark 的對象,用法和 JUnit 的 @Test
類似。
@Mode
Mode
如之前所說,表示 JMH 進行 Benchmark 時所使用的模式。
@State
State
用于聲明某個類是一個“狀態”,然后接受一個 Scope
參數用來表示該狀態的共享范圍。因為很多 benchmark 會需要一些表示狀態的類,JMH 允許你把這些類以依賴注入的方式注入到 benchmark 函數里。Scope
主要分為兩種。
•Thread
: 該狀態為每個線程獨享。•Benchmark
: 該狀態在所有線程間共享。
關于State
的用法,官方的 code sample 里有比較好的例子。
@OutputTimeUnit
benchmark 結果所使用的時間單位。
啟動選項
解釋完了注解,再來看看 JMH 在啟動前設置的參數。
Options opt = new OptionsBuilder()
.include(FirstBenchmark.class.getSimpleName())
.forks(1)
.warmupIterations(5)
.measurementIterations(5)
.build();
new Runner(opt).run();
include
benchmark 所在的類的名字,注意這里是使用正則表達式對所有類進行匹配的。
fork
進行 fork 的次數。如果 fork 數是2的話,則 JMH 會 fork 出兩個進程來進行測試。
warmupIterations
預熱的迭代次數。
measurementIterations
實際測量的迭代次數。
第二個例子
在看過第一個完全只為示范的例子之后,再來看一個有實際意義的例子。
問題:
計算 1 ~ n 之和,比較串行算法和并行算法的效率,看 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();
}
由于這兩種算法的實現不是這篇文章的重點,而且本身并不困難,所以實際代碼就不贅述了。如果真的感興趣的話,可以看最后的附錄。以下僅說明一下我所指的串行算法和并行算法的含義。
•串行算法:使用 for-loop
來計算 n 個正整數之和。•并行算法:將所需要計算的 n 個正整數分成 m 份,交給 m 個線程分別計算出和以后,再把它們的結果相加。
進行 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個之前沒有使用的注解。
@Param
@Param
可以用來指定某項參數的多種情況。特別適合用來測試一個函數在不同的參數輸入的情況下的性能。
@Setup
@Setup
會在執行 benchmark 之前被執行,正如其名,主要用于初始化。
@TearDown
@TearDown
和 @Setup
相對的,會在所有 benchmark 執行結束以后執行,主要用于資源的回收等。
最后來猜猜看實際結果如何?并行算法在哪個問題集下能夠超越串行算法?
我在自己的 mac 上跑下來的結果,總數在10000時并行算法不如串行算法,總數達到100000時并行算法開始和串行算法接近,總數達到1000000時并行算法所耗時間約是串行算法的一半左右。
常用選項
還有一些 JMH 的常用選項沒有提及的,簡單地在此介紹一下
CompilerControl
控制 compiler 的行為,例如強制 inline,不允許編譯等。
Group
可以把多個 benchmark 定義為同一個 group,則它們會被同時執行,主要用于測試多個相互之間存在影響的方法。
Level
用于控制 @Setup
,@TearDown
的調用時機,默認是 Level.Trial
,即benchmark開始前和結束后。
Profiler
JMH 支持一些 profiler,可以顯示等待時間和運行時間比,熱點函數等。
延伸閱讀
IDE插件
IntelliJ 有 JMH 的插件,提供 benchmark 方法的自動生成等便利功能。
JMH 教程
Jenkov 的 JMH 教程,相比于這篇文章介紹得更為詳細,非常推薦。順便 Jenkov 的其他 Java 教程也非常值得一看。
附錄
代碼清單
public class SinglethreadCalculator implements Calculator {
public long sum(int[] numbers) {
long total = 0L;
for (int i : numbers) {
total += i;
}
return total;
}
@Override
public void shutdown() {
// nothing to do
}
}
public class MultithreadCalculator implements Calculator {
private final int nThreads;
private final ExecutorService pool;
public MultithreadCalculator(int nThreads) {
this.nThreads = nThreads;
this.pool = Executors.newFixedThreadPool(nThreads);
}
private class SumTask implements Callable<Long> {
private int[] numbers;
private int from;
private int to;
public SumTask(int[] numbers, int from, int to) {
this.numbers = numbers;
this.from = from;
this.to = to;
}
public Long call() throws Exception {
long total = 0L;
for (int i = from; i < to; i++) {
total += numbers[i];
}
return total;
}
}
public long sum(int[] numbers) {
int chunk = numbers.length / nThreads;
int from, to;
List<SumTask> tasks = new ArrayList<SumTask>();
for (int i = 1; i <= nThreads; i++) {
if (i == nThreads) {
from = (i - 1) * chunk;
to = numbers.length;
} else {
from = (i - 1) * chunk;
to = i * chunk;
}
tasks.add(new SumTask(numbers, from, to));
}
try {
List<Future<Long>> futures = pool.invokeAll(tasks);
long total = 0L;
for (Future<Long> future : futures) {
total += future.get();
}
return total;
} catch (Exception e) {
// ignore
return 0;
}
}
@Override
public void shutdown() {
pool.shutdown();
}
}