性能測試這個話題非常龐大二跋,我們可以從網(wǎng)絡聊到操作系統(tǒng),再從操作系統(tǒng)聊到內(nèi)核,再從內(nèi)核聊到你懷疑人生有木有。
先拍幾個磚出來吧湃交,我在寫代碼的時候經(jīng)常有這種懷疑:寫法A快還是寫法B快,某個位置是用ArrayList還是LinkedList藤巢,HashMap還是TreeMap搞莺,HashMap的初始化size要不要指定,指定之后究竟比默認的DEFAULT_SIZE性能好多少掂咒。才沧。迈喉。
如果你還是通過for循環(huán)或者手擼method來測試你的內(nèi)容的話,那么JMH就是你必須要明白的內(nèi)容了温圆,因為已經(jīng)有人把基準測試的輪子造好了挨摸,接下來我們就一起看看這個輪子怎么用:
JMH只適合細粒度的方法測試,并不適用于系統(tǒng)之間的鏈路測試岁歉!
JMH只適合細粒度的方法測試得运,并不適用于系統(tǒng)之間的鏈路測試!
JMH只適合細粒度的方法測試锅移,并不適用于系統(tǒng)之間的鏈路測試澈圈!
JMH入門:
JMH是一個工具包,如果我們要通過JMH進行基準測試的話帆啃,直接在我們的pom文件中引入JMH的依賴即可:
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>1.19</version>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>1.19</version>
</dependency>
通過一個HelloWorld程序來看一下JMH如果工作:
@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
public class JMHSample_01_HelloWorld {
static class Demo {
int id;
String name;
public Demo(int id, String name) {
this.id = id;
this.name = name;
}
}
static List<Demo> demoList;
static {
demoList = new ArrayList();
for (int i = 0; i < 10000; i ++) {
demoList.add(new Demo(i, "test"));
}
}
@Benchmark
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
public void testHashMapWithoutSize() {
Map map = new HashMap();
for (Demo demo : demoList) {
map.put(demo.id, demo.name);
}
}
@Benchmark
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
public void testHashMap() {
Map map = new HashMap((int)(demoList.size() / 0.75f) + 1);
for (Demo demo : demoList) {
map.put(demo.id, demo.name);
}
}
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(JMHSample_01_HelloWorld.class.getSimpleName())
.forks(1)
.build();
new Runner(opt).run();
}
}
======================================執(zhí)行結果======================================
Benchmark Mode Cnt Score Error Units
JMHSample_01_HelloWorld.testHashMap avgt 5 147.865 ± 81.128 us/op
JMHSample_01_HelloWorld.testHashMapWithoutSize avgt 5 224.897 ± 102.342 us/op
======================================執(zhí)行結果======================================
上面的代碼用中文翻譯一下:分別定義兩個基準測試的方法testHashMapWithoutSize和
testHashMap瞬女,這兩個基準測試方法執(zhí)行流程是:每個方法執(zhí)行前都進行5次預熱執(zhí)行,每隔1秒進行一次預熱操作努潘,預熱執(zhí)行結束之后進行5次實際測量執(zhí)行诽偷,每隔1秒進行一次實際執(zhí)行,我們此次基準測試測量的是平均響應時長疯坤,單位是us报慕。
預熱?為什么要預熱压怠?因為 JVM 的 JIT 機制的存在眠冈,如果某個函數(shù)被調(diào)用多次之后,JVM 會嘗試將其編譯成為機器碼從而提高執(zhí)行速度菌瘫。為了讓 benchmark 的結果更加接近真實情況就需要進行預熱蜗顽。
從上面的執(zhí)行結果我們看出,針對一個Map的初始化參數(shù)的給定其實有很大影響雨让,當我們給定了初始化參數(shù)執(zhí)行執(zhí)行的速度是沒給定參數(shù)的2/3雇盖,這個優(yōu)化速度還是比較明顯的,所以以后大家在初始化Map的時候能給定參數(shù)最好都給定了栖忠,代碼是處處優(yōu)化的崔挖,積少成多。
通過上面的內(nèi)容我們已經(jīng)基本可以看出來JMH的寫法雛形了庵寞,后面的介紹主要是一些注解的使用:
@Benchmark
@Benchmark標簽是用來標記測試方法的狸相,只有被這個注解標記的話,該方法才會參與基準測試捐川,但是有一個基本的原則就是被@Benchmark標記的方法必須是public的脓鹃。
@Warmup
@Warmup用來配置預熱的內(nèi)容,可用于類或者方法上属拾,越靠近執(zhí)行方法的地方越準確将谊。一般配置warmup的參數(shù)有這些:
- iterations:預熱的次數(shù)。
- time:每次預熱的時間渐白。
- timeUnit:時間單位尊浓,默認是s。
- batchSize:批處理大小纯衍,每次操作調(diào)用幾次方法栋齿。(后面用到)
@Measurement
用來控制實際執(zhí)行的內(nèi)容,配置的選項本warmup一樣襟诸。
@BenchmarkMode
@BenchmarkMode主要是表示測量的緯度瓦堵,有以下這些緯度可供選擇:
- Mode.Throughput 吞吐量緯度
- Mode.AverageTime 平均時間
- Mode.SampleTime 抽樣檢測
- Mode.SingleShotTime 檢測一次調(diào)用
- Mode.All 運用所有的檢測模式
在方法級別指定@BenchmarkMode的時候可以一定指定多個緯度,例如:
@BenchmarkMode({Mode.Throughput, Mode.AverageTime, Mode.SampleTime, Mode.SingleShotTime})歌亲,代表同時在多個緯度對目標方法進行測量菇用。
@OutputTimeUnit
@OutputTimeUnit代表測量的單位,比如秒級別陷揪,毫秒級別惋鸥,微妙級別等等。一般都使用微妙和毫秒級別的稍微多一點悍缠。該注解可以用在方法級別和類級別卦绣,當用在類級別的時候會被更加精確的方法級別的注解覆蓋,原則就是離目標更近的注解更容易生效飞蚓。
@State
在很多時候我們需要維護一些狀態(tài)內(nèi)容滤港,比如在多線程的時候我們會維護一個共享的狀態(tài),這個狀態(tài)值可能會在每隔線程中都一樣趴拧,也有可能是每個線程都有自己的狀態(tài)溅漾,JMH為我們提供了狀態(tài)的支持。該注解只能用來標注在類上著榴,因為類作為一個屬性的載體樟凄。 @State的狀態(tài)值主要有以下幾種:
- Scope.Benchmark 該狀態(tài)的意思是會在所有的Benchmark的工作線程中共享變量內(nèi)容。
- Scope.Group 同一個Group的線程可以享有同樣的變量
- Scope.Thread 每隔線程都享有一份變量的副本兄渺,線程之間對于變量的修改不會相互影響缝龄。
下面看兩個常見的@State的寫法:
1.直接在內(nèi)部類中使用@State作為“PropertyHolder”
public class JMHSample_03_States {
@State(Scope.Benchmark)
public static class BenchmarkState {
volatile double x = Math.PI;
}
@State(Scope.Thread)
public static class ThreadState {
volatile double x = Math.PI;
}
@Benchmark
public void measureUnshared(ThreadState state) {
state.x++;
}
@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(4)
.forks(1)
.build();
new Runner(opt).run();
}
}
2.在Main類中直接使用@State作為注解,是Main類直接成為“PropertyHolder”
@State(Scope.Thread)
public class JMHSample_04_DefaultState {
double x = Math.PI;
@Benchmark
public void measure() {
x++;
}
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(JMHSample_04_DefaultState.class.getSimpleName())
.forks(1)
.build();
new Runner(opt).run();
}
}
我們試想以下@State的含義挂谍,它主要是方便框架來控制變量的過程邏輯叔壤,通過@State標示的類都被用作屬性的容器,然后框架可以通過自己的控制來配置不同級別的隔離情況口叙。被@Benchmark標注的方法可以有參數(shù)炼绘,但是參數(shù)必須是被@State注解的,就是為了要控制參數(shù)的隔離妄田。
但是有些情況下我們需要對參數(shù)進行一些初始化或者釋放的操作俺亮,就像Spring提供的一些init和destory方法一樣驮捍,JHM也提供有這樣的鉤子:
- @Setup 必須標示在@State注解的類內(nèi)部,表示初始化操作
- @TearDown 必須表示在@State注解的類內(nèi)部脚曾,表示銷毀操作
初始化和銷毀的動作都只會執(zhí)行一次东且。
@State(Scope.Thread)
public class JMHSample_05_StateFixtures {
double x;
@Setup
public void prepare() {
x = Math.PI;
}
@TearDown
public void check() {
assert x > Math.PI : "Nothing changed?";
}
@Benchmark
public void measureRight() {
x++;
}
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(JMHSample_05_StateFixtures.class.getSimpleName())
.forks(1)
.jvmArgs("-ea")
.build();
new Runner(opt).run();
}
}
雖然我們可以執(zhí)行初始化和銷毀的動作,但是總是感覺還缺點啥本讥?對珊泳,就是初始化的粒度。因為基準測試往往會執(zhí)行多次拷沸,那么能不能保證每次執(zhí)行方法的時候都初始化一次變量呢色查? @Setup和@TearDown提供了以下三種緯度的控制:
- Level.Trial 只會在個基礎測試的前后執(zhí)行。包括Warmup和Measurement階段撞芍,一共只會執(zhí)行一次秧了。
- Level.Iteration 每次執(zhí)行記住測試方法的時候都會執(zhí)行,如果Warmup和Measurement都配置了2次執(zhí)行的話序无,那么@Setup和@TearDown配置的方法的執(zhí)行次數(shù)就4次示惊。
- Level.Invocation 每個方法執(zhí)行的前后執(zhí)行(一般不推薦這么用)
@Param
在很多情況下,我們需要測試不同的參數(shù)的不同結果愉镰,但是測試的了邏輯又都是一樣的米罚,因此如果我們編寫鍍鉻benchmark的話會造成邏輯的冗余,幸好JMH提供了@Param參數(shù)來幫助我們處理這個事情丈探,被@Param注解標示的參數(shù)組會一次被benchmark消費到录择。
@State(Scope.Benchmark)
public class ParamTest {
@Param({"1", "2", "3"})
int testNum;
@Benchmark
public String test() {
return String.valueOf(testNum);
}
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(ParamTest.class.getSimpleName())
.forks(1)
.build();
new Runner(opt).run();
}
}
@Threads
測試線程的數(shù)量,可以配置在方法或者類上碗降,代表執(zhí)行測試的線程數(shù)量隘竭。
通常看到這里我們會比較迷惑Iteration和Invocation區(qū)別讼渊,我們在配置Warmup的時候默認的時間是的1s动看,即1s的執(zhí)行作為一個Iteration,假設每次方法的執(zhí)行是100ms的話爪幻,那么1個Iteration就代表10個Invocation菱皆。
JMH進階
通過以上的內(nèi)容我們已經(jīng)基本可以掌握JMH的使用了,下面就主要介紹一下JMH提供的一些高級特性了挨稿。
不要編寫無用代碼
因為現(xiàn)代的編譯器非常聰明仇轻,如果我們在代碼使用了沒有用處的變量的話,就容易被編譯器優(yōu)化掉奶甘,這就會導致實際的測量結果可能不準確篷店,因為我們要在測量的方法中避免使用void方法,然后記得在測量的結束位置返回結果臭家。這么做的目的很明確疲陕,就是為了與編譯器斗智斗勇方淤,讓編譯器不要改變這段代碼執(zhí)行的初衷。
Blackhole介紹
Blackhole會消費傳進來的值蹄殃,不提供任何信息來確定這些值是否在之后被實際使用携茂。
Blackhole處理的事情主要有以下幾種:
- 死代碼消除:入?yún)撛诿看味急挥玫剑虼司幾g器就不會把這些參數(shù)優(yōu)化為常量或者在計算的過程中對他們進行其他優(yōu)化窃爷。
- 處理內(nèi)存壁:我們需要盡可能減少寫的量,因為它會干擾緩存姓蜂,污染寫緩沖區(qū)等按厘。
這很可能導致過早地撞到內(nèi)存壁
我們在上面說到需要消除無用代碼,那么其中一種方式就是通過Blackhole钱慢,我們可以用Blackhole來消費這些返回的結果逮京。
1:返回測試結果,防止編譯器優(yōu)化
@Benchmark
public double measureRight_1() {
return Math.log(x1) + Math.log(x2);
}
2.通過Blackhole消費中間結果束莫,防止編譯器優(yōu)化
@Benchmark
public void measureRight_2(Blackhole bh) {
bh.consume(Math.log(x1));
bh.consume(Math.log(x2));
}
循環(huán)處理
我們雖然可以在Benchmark中定義循環(huán)邏輯懒棉,但是這么做其實是不合適的,因為編譯器可能會將我們的循環(huán)進行展開或者做一些其他方面的循環(huán)優(yōu)化览绿,所以JHM建議我們不要在Beanchmark中使用循環(huán)策严,如果我們需要處理循環(huán)邏輯了,可以結合@BenchmarkMode(Mode.SingleShotTime)和@Measurement(batchSize = N)來達到同樣的效果.
@State(Scope.Thread)
public class JMHSample_26_BatchSize {
List<String> list = new LinkedList<>();
// 每個iteration中做5000次Invocation
@Benchmark
@Warmup(iterations = 5, batchSize = 5000)
@Measurement(iterations = 5, batchSize = 5000)
@BenchmarkMode(Mode.SingleShotTime)
public List<String> measureRight() {
list.add(list.size() / 2, "something");
return list;
}
@Setup(Level.Iteration)
public void setup(){
list.clear();
}
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(JMHSample_26_BatchSize.class.getSimpleName())
.forks(1)
.build();
new Runner(opt).run();
}
}
方法內(nèi)聯(lián)
方法內(nèi)聯(lián):如果JVM監(jiān)測到一些小方法被頻繁的執(zhí)行饿敲,它會把方法的調(diào)用替換成方法體本身妻导。比如說下面這個:
private int add4(int x1, int x2, int x3, int x4) {
return add2(x1, x2) + add2(x3, x4);
}
private int add2(int x1, int x2) {
return x1 + x2;
}
運行一段時間后JVM會把add2方法去掉,并把你的代碼翻譯成:
private int add4(int x1, int x2, int x3, int x4) {
return x1 + x2 + x3 + x4;
}
JMH提供了CompilerControl注解來控制方法內(nèi)聯(lián)怀各,但是實際上我感覺比較有用的就是兩個了:
- CompilerControl.Mode.DONT_INLINE:強制限制不能使用內(nèi)聯(lián)
- CompilerControl.Mode.INLINE:強制使用內(nèi)聯(lián)
看一下官方提供的例子把:
@State(Scope.Thread)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class JMHSample_16_CompilerControl {
public void target_blank() {
}
@CompilerControl(CompilerControl.Mode.DONT_INLINE)
public void target_dontInline() {
}
@CompilerControl(CompilerControl.Mode.INLINE)
public void target_inline() {
}
@Benchmark
public void baseline() {
}
@Benchmark
public void dontinline() {
target_dontInline();
}
@Benchmark
public void inline() {
target_inline();
}
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(JMHSample_16_CompilerControl.class.getSimpleName())
.warmupIterations(0)
.measurementIterations(3)
.forks(1)
.build();
new Runner(opt).run();
}
}
======================================執(zhí)行結果==============================
Benchmark Mode Cnt Score Error Units
JMHSample_16_CompilerControl.baseline avgt 3 0.896 ± 3.426 ns/op
JMHSample_16_CompilerControl.dontinline avgt 3 0.344 ± 0.126 ns/op
JMHSample_16_CompilerControl.inline avgt 3 0.391 ± 2.622 ns/op
======================================執(zhí)行結果==============================
JMH只適合細粒度的方法測試倔韭,并不適用于系統(tǒng)之間的鏈路測試!
JMH只適合細粒度的方法測試瓢对,并不適用于系統(tǒng)之間的鏈路測試寿酌!
JMH只適合細粒度的方法測試,并不適用于系統(tǒng)之間的鏈路測試硕蛹!
所以醇疼,千萬不要妄想使用JMH做壓測!法焰!