您好,登錄后才能下訂單哦!
如何用JMH進(jìn)行基準(zhǔn)測(cè)試?相信大部分人都還沒學(xué)會(huì)這個(gè)技能,為了讓大家學(xué)會(huì),給大家總結(jié)了以下內(nèi)容,話不多說,一起往下看吧。
JMH實(shí)例:
JMH是一個(gè)工具包,如果我們要通過JMH進(jìn)行基準(zhǔn)測(cè)試的話,直接在我們的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>
通過一個(gè)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í)行結(jié)果======================================
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í)行結(jié)果
上面的代碼用中文翻譯一下:分別定義兩個(gè)基準(zhǔn)測(cè)試的方法testHashMapWithoutSize和 testHashMap,這兩個(gè)基準(zhǔn)測(cè)試方法執(zhí)行流程是:每個(gè)方法執(zhí)行前都進(jìn)行5次預(yù)熱執(zhí)行,每隔1秒進(jìn)行一次預(yù)熱操作,預(yù)熱執(zhí)行結(jié)束之后進(jìn)行5次實(shí)際測(cè)量執(zhí)行,每隔1秒進(jìn)行一次實(shí)際執(zhí)行,我們此次基準(zhǔn)測(cè)試測(cè)量的是平均響應(yīng)時(shí)長(zhǎng),單位是us。
預(yù)熱?為什么要預(yù)熱?因?yàn)?JVM 的 JIT 機(jī)制的存在,如果某個(gè)函數(shù)被調(diào)用多次之后,JVM 會(huì)嘗試將其編譯成為機(jī)器碼從而提高執(zhí)行速度。為了讓 benchmark 的結(jié)果更加接近真實(shí)情況就需要進(jìn)行預(yù)熱。
從上面的執(zhí)行結(jié)果我們看出,針對(duì)一個(gè)Map的初始化參數(shù)的給定其實(shí)有很大影響,當(dāng)我們給定了初始化參數(shù)執(zhí)行執(zhí)行的速度是沒給定參數(shù)的2/3,這個(gè)優(yōu)化速度還是比較明顯的,所以以后大家在初始化Map的時(shí)候能給定參數(shù)最好都給定了,代碼是處處優(yōu)化的,積少成多。
通過上面的內(nèi)容我們已經(jīng)基本可以看出來JMH的寫法雛形了,后面的介紹主要是一些注解的使用:
@Benchmark
@Benchmark標(biāo)簽是用來標(biāo)記測(cè)試方法的,只有被這個(gè)注解標(biāo)記的話,該方法才會(huì)參與基準(zhǔn)測(cè)試,但是有一個(gè)基本的原則就是被@Benchmark標(biāo)記的方法必須是public的。
@Warmup
@Warmup用來配置預(yù)熱的內(nèi)容,可用于類或者方法上,越靠近執(zhí)行方法的地方越準(zhǔn)確。一般配置warmup的參數(shù)有這些:
?iterations:預(yù)熱的次數(shù)。
?time:每次預(yù)熱的時(shí)間。
?timeUnit:時(shí)間單位,默認(rèn)是s。
?batchSize:批處理大小,每次操作調(diào)用幾次方法。(后面用到)
@Measurement
用來控制實(shí)際執(zhí)行的內(nèi)容,配置的選項(xiàng)本warmup一樣。
@BenchmarkMode
@BenchmarkMode主要是表示測(cè)量的緯度,有以下這些緯度可供選擇:
?Mode.Throughput 吞吐量緯度
?Mode.AverageTime 平均時(shí)間
?Mode.SampleTime 抽樣檢測(cè)
?Mode.SingleShotTime 檢測(cè)一次調(diào)用
?Mode.All 運(yùn)用所有的檢測(cè)模式 在方法級(jí)別指定@BenchmarkMode的時(shí)候可以一定指定多個(gè)緯度,例如:@BenchmarkMode({Mode.Throughput, Mode.AverageTime, Mode.SampleTime, Mode.SingleShotTime}),代表同時(shí)在多個(gè)緯度對(duì)目標(biāo)方法進(jìn)行測(cè)量。
?
@OutputTimeUnit
@OutputTimeUnit代表測(cè)量的單位,比如秒級(jí)別,毫秒級(jí)別,微妙級(jí)別等等。一般都使用微妙和毫秒級(jí)別的稍微多一點(diǎn)。該注解可以用在方法級(jí)別和類級(jí)別,當(dāng)用在類級(jí)別的時(shí)候會(huì)被更加精確的方法級(jí)別的注解覆蓋,原則就是離目標(biāo)更近的注解更容易生效。
@State
在很多時(shí)候我們需要維護(hù)一些狀態(tài)內(nèi)容,比如在多線程的時(shí)候我們會(huì)維護(hù)一個(gè)共享的狀態(tài),這個(gè)狀態(tài)值可能會(huì)在每隔線程中都一樣,也有可能是每個(gè)線程都有自己的狀態(tài),JMH為我們提供了狀態(tài)的支持。該注解只能用來標(biāo)注在類上,因?yàn)轭愖鳛橐粋€(gè)屬性的載體。@State的狀態(tài)值主要有以下幾種:
?Scope.Benchmark 該狀態(tài)的意思是會(huì)在所有的Benchmark的工作線程中共享變量?jī)?nèi)容。
?Scope.Group 同一個(gè)Group的線程可以享有同樣的變量
?Scope.Thread 每隔線程都享有一份變量的副本,線程之間對(duì)于變量的修改不會(huì)相互影響。下面看兩個(gè)常見的@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();
}
}
我們?cè)囅胍韵翤State的含義,它主要是方便框架來控制變量的過程邏輯,通過@State標(biāo)示的類都被用作屬性的容器,然后框架可以通過自己的控制來配置不同級(jí)別的隔離情況。被@Benchmark標(biāo)注的方法可以有參數(shù),但是參數(shù)必須是被@State注解的,就是為了要控制參數(shù)的隔離。
但是有些情況下我們需要對(duì)參數(shù)進(jìn)行一些初始化或者釋放的操作,就像Spring提供的一些init和destory方法一樣,JHM也提供有這樣的鉤子:
?@Setup 必須標(biāo)示在@State注解的類內(nèi)部,表示初始化操作
?@TearDown 必須表示在@State注解的類內(nèi)部,表示銷毀操作
?
初始化和銷毀的動(dòng)作都只會(huì)執(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í)行初始化和銷毀的動(dòng)作,但是總是感覺還缺點(diǎn)啥?對(duì),就是初始化的粒度。因?yàn)榛鶞?zhǔn)測(cè)試往往會(huì)執(zhí)行多次,那么能不能保證每次執(zhí)行方法的時(shí)候都初始化一次變量呢?@Setup和@TearDown提供了以下三種緯度的控制:
?Level.Trial 只會(huì)在個(gè)基礎(chǔ)測(cè)試的前后執(zhí)行。包括Warmup和Measurement階段,一共只會(huì)執(zhí)行一次。
?Level.Iteration 每次執(zhí)行記住測(cè)試方法的時(shí)候都會(huì)執(zhí)行,如果Warmup和Measurement都配置了2次執(zhí)行的話,那么@Setup和@TearDown配置的方法的執(zhí)行次數(shù)就4次。
?Level.Invocation 每個(gè)方法執(zhí)行的前后執(zhí)行(一般不推薦這么用)
?
@Param
在很多情況下,我們需要測(cè)試不同的參數(shù)的不同結(jié)果,但是測(cè)試的了邏輯又都是一樣的,因此如果我們編寫鍍鉻benchmark的話會(huì)造成邏輯的冗余,幸好JMH提供了@Param參數(shù)來幫助我們處理這個(gè)事情,被@Param注解標(biāo)示的參數(shù)組會(huì)一次被benchmark消費(fèi)到。
@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
測(cè)試線程的數(shù)量,可以配置在方法或者類上,代表執(zhí)行測(cè)試的線程數(shù)量。
通??吹竭@里我們會(huì)比較迷惑Iteration和Invocation區(qū)別,我們?cè)谂渲肳armup的時(shí)候默認(rèn)的時(shí)間是的1s,即1s的執(zhí)行作為一個(gè)Iteration,假設(shè)每次方法的執(zhí)行是100ms的話,那么1個(gè)Iteration就代表10個(gè)Invocation。
JMH進(jìn)階
通過以上的內(nèi)容我們已經(jīng)基本可以掌握J(rèn)MH的使用了,下面就主要介紹一下JMH提供的一些高級(jí)特性了。
不要編寫無用代碼
因?yàn)楝F(xiàn)代的編譯器非常聰明,如果我們?cè)诖a使用了沒有用處的變量的話,就容易被編譯器優(yōu)化掉,這就會(huì)導(dǎo)致實(shí)際的測(cè)量結(jié)果可能不準(zhǔn)確,因?yàn)槲覀円跍y(cè)量的方法中避免使用void方法,然后記得在測(cè)量的結(jié)束位置返回結(jié)果。這么做的目的很明確,就是為了與編譯器斗智斗勇,讓編譯器不要改變這段代碼執(zhí)行的初衷。
Blackhole介紹
Blackhole會(huì)消費(fèi)傳進(jìn)來的值,不提供任何信息來確定這些值是否在之后被實(shí)際使用。Blackhole處理的事情主要有以下幾種:
?死代碼消除:入?yún)?yīng)該在每次都被用到,因此編譯器就不會(huì)把這些參數(shù)優(yōu)化為常量或者在計(jì)算的過程中對(duì)他們進(jìn)行其他優(yōu)化。
?處理內(nèi)存壁:我們需要盡可能減少寫的量,因?yàn)樗鼤?huì)干擾緩存,污染寫緩沖區(qū)等。這很可能導(dǎo)致過早地撞到內(nèi)存壁
?
我們?cè)谏厦嬲f到需要消除無用代碼,那么其中一種方式就是通過Blackhole,我們可以用Blackhole來消費(fèi)這些返回的結(jié)果。
1:返回測(cè)試結(jié)果,防止編譯器優(yōu)化
@Benchmark
public double measureRight_1() {
return Math.log(x1) + Math.log(x2);
}
2.通過Blackhole消費(fèi)中間結(jié)果,防止編譯器優(yōu)化
@Benchmark
public void measureRight_2(Blackhole bh) {
bh.consume(Math.log(x1));
bh.consume(Math.log(x2));
}
循環(huán)處理
我們雖然可以在Benchmark中定義循環(huán)邏輯,但是這么做其實(shí)是不合適的,因?yàn)榫幾g器可能會(huì)將我們的循環(huán)進(jìn)行展開或者做一些其他方面的循環(huán)優(yōu)化,所以JHM建議我們不要在Beanchmark中使用循環(huán),如果我們需要處理循環(huán)邏輯了,可以結(jié)合@BenchmarkMode(Mode.SingleShotTime)和@Measurement(batchSize = N)來達(dá)到同樣的效果.
@State(Scope.Thread)
public class JMHSample_26_BatchSize {
List<String> list = new LinkedList<>();
// 每個(gè)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)測(cè)到一些小方法被頻繁的執(zhí)行,它會(huì)把方法的調(diào)用替換成方法體本身。比如說下面這個(gè):
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;
}
運(yùn)行一段時(shí)間后JVM會(huì)把a(bǔ)dd2方法去掉,并把你的代碼翻譯成:
private int add4(int x1, int x2, int x3, int x4) {
return x1 + x2 + x3 + x4;
}
JMH提供了CompilerControl注解來控制方法內(nèi)聯(lián),但是實(shí)際上我感覺比較有用的就是兩個(gè)了:
?CompilerControl.Mode.DONT_INLINE:強(qiáng)制限制不能使用內(nèi)聯(lián)
?CompilerControl.Mode.INLINE:強(qiáng)制使用內(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í)行結(jié)果:
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
看完這篇文章,你們學(xué)會(huì)用JMH進(jìn)行基準(zhǔn)測(cè)試了嗎?如果還想學(xué)到更多技能或想了解更多相關(guān)內(nèi)容,歡迎關(guān)注億速云行業(yè)資訊頻道,感謝各位的閱讀。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。