JMH - Java 微基準(zhǔn)測(cè)試工具

accurate-builder-equipment-1573821.jpg

前言

"If you cannot measure it, you cannot improve it".

在日常開發(fā)中,我們對(duì)一些代碼的調(diào)用或者工具的使用會(huì)存在多種選擇方式,在不確定他們性能的時(shí)候,我們首先想要做的就是去測(cè)量它。大多數(shù)時(shí)候,我們會(huì)簡(jiǎn)單的采用多次計(jì)數(shù)的方式來(lái)測(cè)量,來(lái)看這個(gè)方法的總耗時(shí)。

但是,如果熟悉JVM類加載機(jī)制的話,應(yīng)該知道JVM默認(rèn)的執(zhí)行模式是JIT編譯與解釋混合執(zhí)行。JVM通過熱點(diǎn)代碼統(tǒng)計(jì)分析,識(shí)別高頻方法的調(diào)用、循環(huán)體、公共模塊等,基于JIT動(dòng)態(tài)編譯技術(shù),會(huì)將熱點(diǎn)代碼轉(zhuǎn)換成機(jī)器碼,直接交給CPU執(zhí)行。

image.png

也就是說(shuō),JVM會(huì)不斷的進(jìn)行編譯優(yōu)化,這就使得很難確定重復(fù)多少次才能得到一個(gè)穩(wěn)定的測(cè)試結(jié)果?所以,很多有經(jīng)驗(yàn)的同學(xué)會(huì)在測(cè)試代碼前寫一段預(yù)熱的邏輯。

JMH,全稱 Java Microbenchmark Harness (微基準(zhǔn)測(cè)試框架),是專門用于Java代碼微基準(zhǔn)測(cè)試的一套測(cè)試工具API,是由 OpenJDK/Oracle 官方發(fā)布的工具。何謂 Micro Benchmark 呢? 簡(jiǎn)單地說(shuō)就是在 method 層面上的 benchmark,精度可以精確到微秒級(jí)。

Java的基準(zhǔn)測(cè)試需要注意的幾個(gè)點(diǎn):

  • 測(cè)試前需要預(yù)熱。
  • 防止無(wú)用代碼進(jìn)入測(cè)試方法中。
  • 并發(fā)測(cè)試。
  • 測(cè)試結(jié)果呈現(xiàn)。

JMH的使用場(chǎng)景:

  1. 定量分析某個(gè)熱點(diǎn)函數(shù)的優(yōu)化效果
  2. 想定量地知道某個(gè)函數(shù)需要執(zhí)行多長(zhǎng)時(shí)間,以及執(zhí)行時(shí)間和輸入變量的相關(guān)性
  3. 對(duì)比一個(gè)函數(shù)的多種實(shí)現(xiàn)方式

本篇主要是介紹JMH的DEMO演示,和常用的注解參數(shù)。希望能對(duì)你起到幫助。

DEMO 演示

這里先演示一個(gè)DEMO,讓不了解JMH的同學(xué)能夠快速掌握這個(gè)工具的大概用法。

1. 測(cè)試項(xiàng)目構(gòu)建

JMH是內(nèi)置Java9及之后的版本。這里是以Java8進(jìn)行說(shuō)明。

為了方便,這里直接介紹使用maven構(gòu)建JMH測(cè)試項(xiàng)目的方式。

第一種是使用命令行構(gòu)建,在指定目錄下執(zhí)行以下命令:

$ mvn archetype:generate \
          -DinteractiveMode=false \
          -DarchetypeGroupId=org.openjdk.jmh \
          -DarchetypeArtifactId=jmh-java-benchmark-archetype \
          -DgroupId=org.sample \
          -DartifactId=test \
          -Dversion=1.0

對(duì)應(yīng)目錄下會(huì)出現(xiàn)一個(gè)test項(xiàng)目,打開項(xiàng)目后我們會(huì)看到這樣的項(xiàng)目結(jié)構(gòu)。

image.png

第二種方式就是直接在現(xiàn)有的maven項(xiàng)目中添加jmh-corejmh-generator-annprocess的依賴來(lái)集成JMH。

        <dependency>
            <groupId>org.openjdk.jmh</groupId>
            <artifactId>jmh-core</artifactId>
            <version>${jmh.version}</version>
        </dependency>
        <dependency>
            <groupId>org.openjdk.jmh</groupId>
            <artifactId>jmh-generator-annprocess</artifactId>
            <version>${jmh.version}</version>
            <scope>provided</scope>
        </dependency>

2. 編寫性能測(cè)試

這里我以測(cè)試LinkedList 通過index 方式迭代和foreach 方式迭代的性能差距為例子,編寫測(cè)試類,涉及到的注解在之后會(huì)講解,

/**
 * @author Richard_yyf
 * @version 1.0 2019/8/27
 */

@State(Scope.Benchmark)
@OutputTimeUnit(TimeUnit.SECONDS)
@Threads(Threads.MAX)
public class LinkedListIterationBenchMark {
    private static final int SIZE = 10000;

    private List<String> list = new LinkedList<>();
    
    @Setup
    public void setUp() {
        for (int i = 0; i < SIZE; i++) {
            list.add(String.valueOf(i));
        }
    }

    @Benchmark
    @BenchmarkMode(Mode.Throughput)
    public void forIndexIterate() {
        for (int i = 0; i < list.size(); i++) {
            list.get(i);
            System.out.print("");
        }
    }

    @Benchmark
    @BenchmarkMode(Mode.Throughput)
    public void forEachIterate() {
        for (String s : list) {
            System.out.print("");
        }
    }
}

3. 執(zhí)行測(cè)試

運(yùn)行 JMH 基準(zhǔn)測(cè)試有兩種方式,一個(gè)是生產(chǎn)jar文件運(yùn)行,另一個(gè)是直接寫main函數(shù)或者放在單元測(cè)試中執(zhí)行。

生成jar文件的形式主要是針對(duì)一些比較大的測(cè)試,可能對(duì)機(jī)器性能或者真實(shí)環(huán)境模擬有一些需求,需要將測(cè)試方法寫好了放在linux環(huán)境執(zhí)行。具體命令如下

$ mvn clean install
$ java -jar target/benchmarks.jar

我們?nèi)粘V杏龅降囊话闶且恍┬y(cè)試,比如我上面寫的例子,直接在IDE中跑就好了。啟動(dòng)方式如下:

 public static void main(String[] args) throws RunnerException {
        Options opt = new OptionsBuilder()
                .include(LinkedListIterationBenchMark.class.getSimpleName())
                .forks(1)
                .warmupIterations(2)
                .measurementIterations(2)
                .output("E:/Benchmark.log")
                .build();

        new Runner(opt).run();
    }

4. 報(bào)告結(jié)果

輸出結(jié)果如下,

最后的結(jié)果:

Benchmark                                      Mode  Cnt     Score   Error  Units
LinkedListIterationBenchMark.forEachIterate   thrpt    2  1192.380          ops/s
LinkedListIterationBenchMark.forIndexIterate  thrpt    2   206.866          ops/s

整個(gè)過程:

# Detecting actual CPU count: 12 detected
# JMH version: 1.21
# VM version: JDK 1.8.0_131, Java HotSpot(TM) 64-Bit Server VM, 25.131-b11
# VM invoker: C:\Program Files\Java\jdk1.8.0_131\jre\bin\java.exe
# VM options: -javaagent:D:\Program Files\JetBrains\IntelliJ IDEA 2018.2.2\lib\idea_rt.jar=65175:D:\Program Files\JetBrains\IntelliJ IDEA 2018.2.2\bin -Dfile.encoding=UTF-8
# Warmup: 2 iterations, 10 s each
# Measurement: 2 iterations, 10 s each
# Timeout: 10 min per iteration
# Threads: 12 threads, will synchronize iterations
# Benchmark mode: Throughput, ops/time
# Benchmark: org.sample.jmh.LinkedListIterationBenchMark.forEachIterate

# Run progress: 0.00% complete, ETA 00:01:20
# Fork: 1 of 1
# Warmup Iteration   1: 1189.267 ops/s
# Warmup Iteration   2: 1197.321 ops/s
Iteration   1: 1193.062 ops/s
Iteration   2: 1191.698 ops/s


Result "org.sample.jmh.LinkedListIterationBenchMark.forEachIterate":
  1192.380 ops/s


# JMH version: 1.21
# VM version: JDK 1.8.0_131, Java HotSpot(TM) 64-Bit Server VM, 25.131-b11
# VM invoker: C:\Program Files\Java\jdk1.8.0_131\jre\bin\java.exe
# VM options: -javaagent:D:\Program Files\JetBrains\IntelliJ IDEA 2018.2.2\lib\idea_rt.jar=65175:D:\Program Files\JetBrains\IntelliJ IDEA 2018.2.2\bin -Dfile.encoding=UTF-8
# Warmup: 2 iterations, 10 s each
# Measurement: 2 iterations, 10 s each
# Timeout: 10 min per iteration
# Threads: 12 threads, will synchronize iterations
# Benchmark mode: Throughput, ops/time
# Benchmark: org.sample.jmh.LinkedListIterationBenchMark.forIndexIterate

# Run progress: 50.00% complete, ETA 00:00:40
# Fork: 1 of 1
# Warmup Iteration   1: 205.676 ops/s
# Warmup Iteration   2: 206.512 ops/s
Iteration   1: 206.542 ops/s
Iteration   2: 207.189 ops/s


Result "org.sample.jmh.LinkedListIterationBenchMark.forIndexIterate":
  206.866 ops/s


# Run complete. Total time: 00:01:21

REMEMBER: The numbers below are just data. To gain reusable insights, you need to follow up on
why the numbers are the way they are. Use profilers (see -prof, -lprof), design factorial
experiments, perform baseline and negative tests that provide experimental control, make sure
the benchmarking environment is safe on JVM/OS/HW level, ask for reviews from the domain experts.
Do not assume the numbers tell you what you want them to tell.

Benchmark                                      Mode  Cnt     Score   Error  Units
LinkedListIterationBenchMark.forEachIterate   thrpt    2  1192.380          ops/s
LinkedListIterationBenchMark.forIndexIterate  thrpt    2   206.866          ops/s

注解介紹

下面我們來(lái)詳細(xì)介紹一下相關(guān)的注解,

@BenchmarkMode

微基準(zhǔn)測(cè)試類型。 JMH 提供了以下幾種類型進(jìn)行支持:

類型 描述
Throughput 每段時(shí)間執(zhí)行的次數(shù),一般是秒
AverageTime 平均時(shí)間,每次操作的平均耗時(shí)
SampleTime 在測(cè)試中,隨機(jī)進(jìn)行采樣執(zhí)行的時(shí)間
SingleShotTime 在每次執(zhí)行中計(jì)算耗時(shí)
All 所有模式

可以注釋在方法級(jí)別,也可以注釋在類級(jí)別,

@BenchmarkMode(Mode.All)
public class LinkedListIterationBenchMark {
    ...
}
@Benchmark
@BenchmarkMode({Mode.Throughput, Mode.SingleShotTime})
public void m() {
    ...
}

@Warmup

這個(gè)單詞的意思就是預(yù)熱,iterations = 3就是指預(yù)熱輪數(shù)。

@Benchmark
@BenchmarkMode({Mode.Throughput, Mode.SingleShotTime})
@Warmup(iterations = 3)
public void m() {
    ...
}

@Measurement

正式度量計(jì)算的輪數(shù)。

  • iterations 進(jìn)行測(cè)試的輪次
  • time 每輪進(jìn)行的時(shí)長(zhǎng)
  • timeUnit時(shí)長(zhǎng)單位
@Benchmark
@BenchmarkMode({Mode.Throughput, Mode.SingleShotTime})
@Measurement(iterations = 3)
public void m() {
    ...
}

@Threads

每個(gè)進(jìn)程中的測(cè)試線程。

@Threads(Threads.MAX)
public class LinkedListIterationBenchMark {
    ...
}

@Fork

進(jìn)行 fork 的次數(shù)。如果 fork 數(shù)是3的話,則 JMH 會(huì) fork 出3個(gè)進(jìn)程來(lái)進(jìn)行測(cè)試。

@Benchmark
@BenchmarkMode({Mode.Throughput, Mode.SingleShotTime})
@Fork(value = 3)
public void m() {
    ...
}

@OutputTimeUnit

基準(zhǔn)測(cè)試結(jié)果的時(shí)間類型。一般選擇秒、毫秒、微秒。

@OutputTimeUnit(TimeUnit.SECONDS)
public class LinkedListIterationBenchMark {
    ...
}

@Benchmark

方法級(jí)注解,表示該方法是需要進(jìn)行 benchmark 的對(duì)象,用法和 JUnit 的 @Test 類似。

@Param

屬性級(jí)注解,@Param 可以用來(lái)指定某項(xiàng)參數(shù)的多種情況。特別適合用來(lái)測(cè)試一個(gè)函數(shù)在不同的參數(shù)輸入的情況下的性能。

@Setup

方法級(jí)注解,這個(gè)注解的作用就是我們需要在測(cè)試之前進(jìn)行一些準(zhǔn)備工作,比如對(duì)一些數(shù)據(jù)的初始化之類的。

@TearDown

方法級(jí)注解,這個(gè)注解的作用就是我們需要在測(cè)試之后進(jìn)行一些結(jié)束工作,比如關(guān)閉線程池,數(shù)據(jù)庫(kù)連接等的,主要用于資源的回收等。

@State

當(dāng)使用@Setup參數(shù)的時(shí)候,必須在類上加這個(gè)參數(shù),不然會(huì)提示無(wú)法運(yùn)行。

就比如我上面的例子中,就必須設(shè)置state。

State 用于聲明某個(gè)類是一個(gè)“狀態(tài)”,然后接受一個(gè) Scope 參數(shù)用來(lái)表示該狀態(tài)的共享范圍。 因?yàn)楹芏?benchmark 會(huì)需要一些表示狀態(tài)的類,JMH 允許你把這些類以依賴注入的方式注入到 benchmark 函數(shù)里。Scope 主要分為三種。

  1. Thread: 該狀態(tài)為每個(gè)線程獨(dú)享。
  2. Group: 該狀態(tài)為同一個(gè)組里面所有線程共享。
  3. Benchmark: 該狀態(tài)在所有線程間共享。

啟動(dòng)方法

在啟動(dòng)方法中,可以直接指定上述說(shuō)到的一些參數(shù),并且能將測(cè)試結(jié)果輸出到指定文件中,

    /**
     * 僅限于IDE中運(yùn)行
     * 命令行模式 則是 build 然后 java -jar 啟動(dòng)
     *
     * 1. 這是benchmark 啟動(dòng)的入口
     * 2. 這里同時(shí)還完成了JMH測(cè)試的一些配置工作
     * 3. 默認(rèn)場(chǎng)景下,JMH會(huì)去找尋標(biāo)注了@Benchmark的方法,可以通過include和exclude兩個(gè)方法來(lái)完成包含以及排除的語(yǔ)義
     */
    public static void main(String[] args) throws RunnerException {
        Options opt = new OptionsBuilder()
                // 包含語(yǔ)義
                // 可以用方法名,也可以用XXX.class.getSimpleName()
                .include("Helloworld")
                // 排除語(yǔ)義
                .exclude("Pref")
                // 預(yù)熱10輪
                .warmupIterations(10)
                // 代表正式計(jì)量測(cè)試做10輪,
                // 而每次都是先執(zhí)行完預(yù)熱再執(zhí)行正式計(jì)量,
                // 內(nèi)容都是調(diào)用標(biāo)注了@Benchmark的代碼。
                .measurementIterations(10)
                //  forks(3)指的是做3輪測(cè)試,
                // 因?yàn)橐淮螠y(cè)試無(wú)法有效的代表結(jié)果,
                // 所以通過3輪測(cè)試較為全面的測(cè)試,
                // 而每一輪都是先預(yù)熱,再正式計(jì)量。
                .forks(3)
                .output("E:/Benchmark.log")
                .build();

        new Runner(opt).run();
    }

結(jié)語(yǔ)

基于JMH可以對(duì)很多工具和框架進(jìn)行測(cè)試,比如日志框架性能對(duì)比、BeanCopy性能對(duì)比 等,更多的example可以參考官方給出的JMH samples

上面其實(shí)只是講解了關(guān)于JMH的使用,推薦延伸閱讀這篇文章

JAVA拾遺 — JMH與8個(gè)測(cè)試陷阱

作者從 Java Developer 角度來(lái)談?wù)勔恍┏R姷拇a測(cè)試陷阱,分析他們和操作系統(tǒng)底層以及 Java 底層的關(guān)聯(lián)性,并借助 JMH 來(lái)幫助大家擺脫這些陷阱。

參考

  1. http://hg.openjdk.java.net/code-tools/jmh/file/tip/jmh-samples/src/main/java/org/openjdk/jmh/samples/
  2. http://www.hollischuang.com/archives/1072
  3. https://yq.aliyun.com/articles/341539?utm_content=m_39911
  4. https://openjdk.java.net/projects/code-tools/jmh/
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容