基準測試神器 - JMH [ Java Microbenchmark Harness ]

Table of Contents

一. 簡介

二. 安裝 [ idea plug ]

三. 註解

@Benchmark

@Warmup

@Measurement

@BenchmarkMode

@OutputTimeUnit

@State

@Param

@Threads

四.使用樣例

         4.1.修改pom.xml配置文件

4.2.測試map的循環輸出效率

4.2.1.代碼

4.2.2.結果:

4.2.3.原因.

4.3.測試list的循環效果

4.3.1.代碼

4.3.2. 結果

五.官方網站&示例


 

 

一. 簡介

JMH,全稱 Java Microbenchmark Harness (微基準測試框架),是專門用於Java代碼微基準測試的一套測試工具API,是由 OpenJDK/Oracle 官方發佈的工具。其精度可以達到毫秒級.可以執行一個函數需要多少時間,或者一個算法有多種不同實現等情況下,選擇一個性能最好的那個.

 

Java的基準測試需要注意的幾個點:

  • 測試前需要預熱。
  • 防止無用代碼進入測試方法中。
  • 併發測試。
  • 測試結果呈現。

預熱?   爲什麼要預熱? ???????????

因爲 JVM 的 JIT 機制的存在,如果某個函數被調用多次之後,JVM 會嘗試將其編譯成爲機器碼從而提高執行速度。爲了讓 benchmark 的結果更加接近真實情況就需要進行預熱。

JMH的使用場景:

  1. 定量分析某個熱點函數的優化效果
  2. 想定量地知道某個函數需要執行多長時間,以及執行時間和輸入變量的相關性
  3. 對比一個函數的多種實現方式

 

 

 

二. 安裝 [ idea plug ]

2.1 直接在Plugins的面板搜索 JMH  安裝JMH plugin . 

 

 

 2.2 配置idea , 即可. 

 

 

三. 註解

 

@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註解的類內部,表示銷燬操作

初始化和銷燬的動作都只會執行一次。



import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.OptionsBuilder;
import org.openjdk.jmh.runner.options.Options;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.TimeUnit;


@BenchmarkMode(Mode.AverageTime) // 測試完成時間
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Warmup(iterations = 2, time = 1, timeUnit = TimeUnit.SECONDS) // 預熱 2 輪,每次 1s
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) // 測試 5 輪,每次 3s
@Fork(1) // fork 1 個線程
@State(Scope.Thread) // 每個測試線程一個實例
public class JMHSample_05_StateFixtures {
    double x;

    @Setup
    public void prepare() {
        System.out.println("This is Setup ");
        x = Math.PI;
    }

    @TearDown
    public void check() {
        System.out.println("This is TearDown ");
        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。

 

 

 

四.使用樣例

4.1.修改pom.xml配置文件

   <dependencies>
        <!-- https://mvnrepository.com/artifact/org.openjdk.jmh/jmh-core -->
        <dependency>
            <groupId>org.openjdk.jmh</groupId>
            <artifactId>jmh-core</artifactId>
            <version>1.23</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.openjdk.jmh/jmh-generator-annprocess -->
        <dependency>
            <groupId>org.openjdk.jmh</groupId>
            <artifactId>jmh-generator-annprocess</artifactId>
            <version>1.23</version>
            <scope>provided</scope>
        </dependency>

    </dependencies>

 

4.2.測試map的循環輸出效率

4.2.1.代碼

package com.map;

import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.OptionsBuilder;
import org.openjdk.jmh.runner.options.Options;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.TimeUnit;

@BenchmarkMode(Mode.AverageTime) // 測試完成時間
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Warmup(iterations = 2, time = 1, timeUnit = TimeUnit.SECONDS) // 預熱 2 輪,每次 1s
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) // 測試 5 輪,每次1s
@Fork(1) // fork 1 個線程
@State(Scope.Thread) // 每個測試線程一個實例
public class HashMapTest {
    static Map<Integer, String> map = new HashMap() {{
        // 添加數據
        for (int i = 0; i < 10000; i++) {
            put(i, "val:" + i);
        }
    }};

    public static void main(String[] args) throws RunnerException {
        // 啓動基準測試
        Options opt = new OptionsBuilder()
                .include(HashMapTest.class.getSimpleName()) // 要導入的測試類
//                .output("/a/jmh-map.log") // 輸出測試結果的文件
                .build();
        new Runner(opt).run(); // 執行測試
    }

    @Benchmark
    public void entrySet() {
        // 遍歷
        Iterator<Map.Entry<Integer, String>> iterator = map.entrySet().iterator();
        while (iterator.hasNext()) {
            Map.Entry<Integer, String> entry = iterator.next();
            Integer k = entry.getKey();
            String v = entry.getValue();
        }
    }

    @Benchmark
    public void forEachEntrySet() {
        // 遍歷
        for (Map.Entry<Integer, String> entry : map.entrySet()) {
            Integer k = entry.getKey();
            String v = entry.getValue();
        }
    }

    @Benchmark
    public void keySet() {
        // 遍歷
        Iterator<Integer> iterator = map.keySet().iterator();
        while (iterator.hasNext()) {
            Integer k = iterator.next();
            String v = map.get(k);
        }
    }

    @Benchmark
    public void forEachKeySet() {
        // 遍歷
        for (Integer key : map.keySet()) {
            Integer k = key;
            String v = map.get(k);
        }
    }

    @Benchmark
    public void lambda() {
        // 遍歷
        map.forEach((key, value) -> {
            Integer k = key;
            String v = map.get(k);
        });
    }

    @Benchmark
    public void streamApi() {
        // 單線程遍歷
        map.entrySet().stream().forEach((entry) -> {
            Integer k = entry.getKey();
            String v = entry.getValue();
        });
    }

    public void parallelStreamApi() {
        // 多線程遍歷
        map.entrySet().parallelStream().forEach((entry) -> {
            Integer k = entry.getKey();
            String v = entry.getValue();
        });
    }
}

4.2.2.結果:

儘量使用  map.entrySet() 進行輸出.  因爲性能方面照普通的循環快將近2倍.

Benchmark                    Mode  Cnt      Score       Error  Units
HashMapTest.entrySet         avgt    5  47831.209 ± 14037.126  ns/op
HashMapTest.forEachEntrySet  avgt    5  56189.356 ± 16866.246  ns/op
HashMapTest.forEachKeySet    avgt    5  75973.584 ±  7738.707  ns/op
HashMapTest.keySet           avgt    5  76458.791 ± 11008.914  ns/op
HashMapTest.lambda           avgt    5  80734.554 ±  9695.806  ns/op
HashMapTest.streamApi        avgt    5  49939.633 ± 11131.095  ns/op

4.2.3.原因.

EntrySet 之所以比 KeySet 的性能高是因爲,KeySet 在循環時使用了 map.get(key),而 map.get(key) 相當於又遍歷了一遍 Map 集合去查詢 key 所對應的值。爲什麼要用“又”這個詞?那是因爲在使用迭代器或者 for 循環時,其實已經遍歷了一遍 Map 集合了,因此再使用 map.get(key) 查詢時,相當於遍歷了兩遍

而 EntrySet 只遍歷了一遍 Map 集合,之後通過代碼“Entry<Integer, String> entry = iterator.next()”把對象的 key 和 value 值都放入到了 Entry 對象中,因此再獲取 key 和 value 值時就無需再遍歷 Map 集合,只需要從 Entry 對象中取值就可以了。

所以,EntrySet 的性能比 KeySet 的性能高出了一倍,因爲 KeySet 相當於循環了兩遍 Map 集合,而 EntrySet 只循環了一遍

 

4.3.測試list的循環效果

4.3.1.代碼

package list;


import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.OptionsBuilder;
import org.openjdk.jmh.runner.options.Options;

import java.util.*;
import java.util.concurrent.TimeUnit;

@BenchmarkMode(Mode.AverageTime) // 測試完成時間
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Warmup(iterations = 2, time = 1, timeUnit = TimeUnit.SECONDS) // 預熱 2 輪,每次 1s
@Measurement(iterations = 5, time = 3, timeUnit = TimeUnit.SECONDS) // 測試 5 輪,每次 3s
@Fork(1) // fork 1 個線程
@State(Scope.Thread) // 每個測試線程一個實例
public class ListTest {

    static LinkedList<Integer> linkedList = new LinkedList<Integer>(){
        {
            // 添加數據
            for (int i = 0; i < 10000; i++) {
                add(i);
            }
        }
    };

    static ArrayList<Integer> arrayList = new ArrayList<Integer>(){
        {
            // 添加數據
            for (int i = 0; i < 10000; i++) {
                add(i);
            }
        }
    };


    public static void main(String[] args) throws RunnerException {
        // 啓動基準測試
        Options opt = new OptionsBuilder()
                .include(ListTest.class.getSimpleName()) // 要導入的測試類
//                .output("/a/jmh-map.log") // 輸出測試結果的文件
                .build();
        new Runner(opt).run(); // 執行測試
    }

    @Benchmark
    public void linkedListFor() {
        // 遍歷
        for (int i = 0 ; i < linkedList.size() ; i++) {
            int x = linkedList.get(i);
        }
    }

    @Benchmark
    public void linkedListForEach() {
        // 遍歷
        for (Integer i  : linkedList ) {

        }
    }

    @Benchmark
    public void arrayListFor() {
        // 遍歷
        for (int i = 0 ; i < arrayList.size() ; i++) {
            int x = arrayList.get(i);
        }
    }

    @Benchmark
    public void arrayListForEach() {
        // 遍歷
        for (Integer i  : arrayList ) {

        }
    }

}

 

4.3.2. 結果

Benchmark                   Mode  Cnt         Score         Error  Units
ListTest.arrayListFor       avgt    5      7291.515 ±    1348.462  ns/op
ListTest.arrayListForEach   avgt    5      6955.242 ±    1587.509  ns/op
ListTest.linkedListFor      avgt    5  41467520.339 ± 8443367.361  ns/op
ListTest.linkedListForEach  avgt    5     22096.751 ±    6459.100  ns/op

 

 

 

五.官方網站&示例

 

官網: https://openjdk.java.net/projects/code-tools/jmh/

git hub idea-jmh-plugin : https://github.com/artyushov/idea-jmh-plugin

示例: https://hg.openjdk.java.net/code-tools/jmh/file/tip/jmh-samples/src/main/java/org/openjdk/jmh/samples/
 

 

 

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章