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