架構師Java 併發基準測試神器的-JMH,程序員必看!

架構師Java 併發基準測試神器的-JMH,程序員必看!程序員

架構師Java 併發基準測試神器的-JMH,程序員必看!
在Java編程這個行業裏面性能測試這個話題很是龐大,咱們能夠從網絡聊到操做系統,再從操做系統聊到內核,再從內核聊到你懷疑人生有木有。編程

先拍幾個磚出來吧,我在寫代碼的時候常常有這種懷疑:寫法A快仍是寫法B快,某個位置是用ArrayList仍是LinkedList,HashMap仍是TreeMap,HashMap的初始化size要不要指定,指定以後究竟比默認的DEFAULT_SIZE性能好多少。。。數組

若是你仍是經過for循環或者手擼method來測試你的內容的話,那麼JMH就是你必需要明白的內容了,由於已經有人把基準測試的輪子造好了,接下來咱們就一塊兒看看這個輪子怎麼用:緩存

JMH只適合細粒度的方法測試,並不適用於系統之間的鏈路測試!網絡

JMH只適合細粒度的方法測試,並不適用於系統之間的鏈路測試!多線程

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

======================================執行結果==============================

重磅!碼農突圍-技術交流羣已成立

你們可添加碼農突圍助手,可申請加入碼農突圍大羣和細分方向羣,細分方向已涵蓋:Java、Python、機器學習、大數據、人工智能等羣。

專一於Java架構師技術分享,撩我免費送Java全套架構師晉級資料

(Java架構師交流企*----:445--820-*-908 )

相關文章
相關標籤/搜索