使用JMH進行基準測試

性能測試這個話題非常龐大二跋,我們可以從網(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做壓測!法焰!

?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末僵腺,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子壶栋,更是在濱河造成了極大的恐慌辰如,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,657評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件贵试,死亡現(xiàn)場離奇詭異琉兜,居然都是意外死亡凯正,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,889評論 3 394
  • 文/潘曉璐 我一進店門豌蟋,熙熙樓的掌柜王于貴愁眉苦臉地迎上來廊散,“玉大人,你說我怎么就攤上這事梧疲≡识茫” “怎么了?”我有些...
    開封第一講書人閱讀 164,057評論 0 354
  • 文/不壞的土叔 我叫張陵幌氮,是天一觀的道長缭受。 經(jīng)常有香客問我,道長该互,這世上最難降的妖魔是什么米者? 我笑而不...
    開封第一講書人閱讀 58,509評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮宇智,結果婚禮上蔓搞,老公的妹妹穿的比我還像新娘。我一直安慰自己随橘,他們只是感情好喂分,可當我...
    茶點故事閱讀 67,562評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著机蔗,像睡著了一般妻顶。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上蜒车,一...
    開封第一講書人閱讀 51,443評論 1 302
  • 那天讳嘱,我揣著相機與錄音,去河邊找鬼酿愧。 笑死沥潭,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的嬉挡。 我是一名探鬼主播钝鸽,決...
    沈念sama閱讀 40,251評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼庞钢!你這毒婦竟也來了拔恰?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,129評論 0 276
  • 序言:老撾萬榮一對情侶失蹤基括,失蹤者是張志新(化名)和其女友劉穎颜懊,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,561評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡河爹,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,779評論 3 335
  • 正文 我和宋清朗相戀三年匠璧,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片咸这。...
    茶點故事閱讀 39,902評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡夷恍,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出媳维,到底是詐尸還是另有隱情酿雪,我是刑警寧澤,帶...
    沈念sama閱讀 35,621評論 5 345
  • 正文 年R本政府宣布侄刽,位于F島的核電站指黎,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏唠梨。R本人自食惡果不足惜袋励,卻給世界環(huán)境...
    茶點故事閱讀 41,220評論 3 328
  • 文/蒙蒙 一侥啤、第九天 我趴在偏房一處隱蔽的房頂上張望当叭。 院中可真熱鬧,春花似錦盖灸、人聲如沸蚁鳖。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,838評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽醉箕。三九已至,卻和暖如春徙垫,著一層夾襖步出監(jiān)牢的瞬間讥裤,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,971評論 1 269
  • 我被黑心中介騙來泰國打工姻报, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留己英,地道東北人。 一個月前我還...
    沈念sama閱讀 48,025評論 2 370
  • 正文 我出身青樓吴旋,卻偏偏與公主長得像损肛,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子荣瑟,可洞房花燭夜當晚...
    茶點故事閱讀 44,843評論 2 354

推薦閱讀更多精彩內(nèi)容

  • 內(nèi)容已經(jīng)移動到這里[https://blog.csdn.net/WeiPeng2K/article/details...
    weipeng2k閱讀 4,005評論 0 6
  • Spring Cloud為開發(fā)人員提供了快速構建分布式系統(tǒng)中一些常見模式的工具(例如配置管理治拿,服務發(fā)現(xiàn),斷路器笆焰,智...
    卡卡羅2017閱讀 134,656評論 18 139
  • 摘要:# 使用JMH做Java微基準測試 在使用Java編程過程中劫谅,我們對于一些代碼調(diào)用的細節(jié)有多種編寫方式,但是...
    Java架構師Carl閱讀 429評論 0 0
  • 遇到問題,去哪找鑰匙同波,就是說怎么辦 冷靜下來鳄梅,情商變高,去更遠地方未檩,看更大的世界戴尸,眼界開闊了,才能更沉靜冤狡。 慢慢進...
    可愛的三姑閱讀 151評論 0 0
  • (三)天堂的愛 劉傳道同師母和教會小組的眾姊妹齊聚妮子家孙蒙,剖析著妮子的罪,講魔鬼撒旦侵占了她的心悲雳,才...
    波羅子閱讀 1,021評論 0 1