溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

如何用JMH進(jìn)行基準(zhǔn)測(cè)試

發(fā)布時(shí)間:2020-06-03 16:12:44 來源:億速云 閱讀:272 作者:Leah 欄目:編程語言

如何用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è)資訊頻道,感謝各位的閱讀。

向AI問一下細(xì)節(jié)

免責(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)容。

AI