JMH – Java基準測試

2023-07-03 18:00:17

官方資源

官方Github樣例

應用場景

  • 對要使用的資料結構不確定,不知道誰的效能更好
  • 對歷史方法程式碼重構,要評判改造之後的效能提升多少 (我要做的場景
  • 想準確地知道某個方法需要執行多長時間,以及執行時間和輸入之間的相關性
  • 對比介面不同實現在給定條件下的吞吐量
  • 檢視多少百分比的請求在多長時間內完成

背景限制(防槓指南)

  • 業務場景?
    因為當前專案是接手比較老的專案,已經有成熟業務在跑,原先的生成模型是nextByCalendarAndRandom, 序號生成是採用兩位亂數,然後亂數產生了衝突,一毫秒內產生的兩個亂數有衝突,
  • 為什麼不直接使用 snowflake?
    原先的生成邏輯 6(商戶號) + 15(yyMMddHHmmssSSS 最大長度,可能比15小) + 2(亂數) = 23 (最大長度)
    如果使用雪花演演算法,則 6 + 19 = 25 (最大長度),且現在業務方較多,不確定對方是否有限制該欄位長度,再就是如果對雪花演演算法進行裁剪,也不能保證肯定不會出現衝突,經衡量過後,暫時不使用雪花演演算法,後續業務方能確定長度沒有問題,就可以升級
  • 這個演演算法不是分散式的,如果是兩臺伺服器,則出現衝突的可能性就變大了
    是的,如果兩臺服務同時執行,然後又同時有請求進來,就有很大的可能性出現衝突,但現在的業務狀況是單體架構,只不過做了主備服務,主服務宕機,備份才會啟動,暫時不會兩臺服務同時啟動
  • 那如果採用 nextByCalendarAndAtomicInteger 自增,就表示一毫秒最大隻有100個請求能進來?超過就肯定會衝突?
    是的,這個也是業務決定的,如果我們當前的業務量超過每毫秒超100,那問題可能不是我這裡的衝突了,服務會率先被壓垮
  • 最終的業務採用什麼方法?
    使用了 nextByLocalDateTimeAndAtomicInteger 方法,也有每毫秒超100必定重複的限制

參照依賴

        <dependency>
            <groupId>org.openjdk.jmh</groupId>
            <artifactId>jmh-core</artifactId>
            <version>1.35</version>
        </dependency>
        <dependency>
            <groupId>org.openjdk.jmh</groupId>
            <artifactId>jmh-generator-annprocess</artifactId>
            <version>1.35</version>
        </dependency>

測試程式碼

@UtilityClass
public class IdWork {

    @Deprecated
    public static String nextByCalendarAndRandom(String merchantNo) {

        Calendar now = Calendar.getInstance();
        long random1 = Math.round(Math.random() * 9);
        long random2 = Math.round(Math.random() * 9);
        String timestamp = (now.get(Calendar.YEAR) + "").substring(2)
                + (now.get(Calendar.MONTH) + 1)
                + now.get(Calendar.DAY_OF_MONTH)
                + now.get(Calendar.HOUR_OF_DAY)
                + now.get(Calendar.MINUTE)
                + now.get(Calendar.SECOND)
                + now.get(Calendar.MILLISECOND);
        return merchantNo + timestamp + random1 + random2;
    }

    @Deprecated
    public static String nextByLocalDateTimeAndRandom(String merchantNo) {

        LocalDateTime now = LocalDateTime.now();
        long random1 = Math.round(Math.random() * 9);
        long random2 = Math.round(Math.random() * 9);
        String timestamp = (now.getYear() + "").substring(2)
                + now.getMonthValue()
                + now.getDayOfMonth()
                + now.getHour()
                + now.getMinute()
                + now.getSecond()
                + (now.getNano() / 1000000);
        return merchantNo + timestamp + random1 + random2;
    }

    @Deprecated
    public static String nextByCalendarAndAtomicInteger(String merchantNo) {

        Calendar now = Calendar.getInstance();
        String timestamp = (now.get(Calendar.YEAR) + "").substring(2)
                + (now.get(Calendar.MONTH) + 1)
                + now.get(Calendar.DAY_OF_MONTH)
                + now.get(Calendar.HOUR_OF_DAY)
                + now.get(Calendar.MINUTE)
                + now.get(Calendar.SECOND)
                + now.get(Calendar.MILLISECOND);
        return merchantNo + timestamp + getSeqNo();
    }

    @Deprecated
    public static String nextByLocalDateTimeAndAtomicInteger(String merchantNo) {

        LocalDateTime now = LocalDateTime.now();
        String timestamp = (now.getYear() + "").substring(2)
                + now.getMonthValue()
                + now.getDayOfMonth()
                + now.getHour()
                + now.getMinute()
                + now.getSecond()
                + (now.getNano() / 1000000);
        return merchantNo + timestamp + getSeqNo();
    }

    public static String nextBySnowflake(String merchantNo) {
        return merchantNo + IdGenerator.next();
    }

    private static AtomicInteger seqNo = new AtomicInteger(1);

    private static String getSeqNo() {

        int curSeqNo = seqNo.getAndIncrement();
        if (curSeqNo > 99) { // 重置,也可以取模
            seqNo = new AtomicInteger(1);
        }
        if (curSeqNo < 10) {
            return "0" + curSeqNo;
        }
        return curSeqNo + "";

    }

    public static void main(String[] args) {
        String next1 = IdWork.nextByCalendarAndRandom("900087");
        System.out.println(next1);
        String next2 = IdWork.nextByLocalDateTimeAndRandom("900087");
        System.out.println(next2);
        String next3 = IdWork.nextByCalendarAndAtomicInteger("900087");
        System.out.println(next3);
        String next4 = IdWork.nextByLocalDateTimeAndAtomicInteger("900087");
        System.out.println(next4);
        String next5 = IdWork.nextBySnowflake("900087");
        System.out.println(next5);
    }
}
public class IdTest {


    @Benchmark
    public String getIdBySnowflake() {
        return IdWork.nextBySnowflake("900087");
    }

    @Benchmark
    public String nextByCalendarAndRandom() {
        return IdWork.nextByCalendarAndRandom("900087");
    }

    @Benchmark
    public String nextByLocalDateTimeAndRandom() {
        return IdWork.nextByLocalDateTimeAndRandom("900087");
    }

    @Benchmark
    public String nextByCalendarAndAtomicInteger() {
        return IdWork.nextByCalendarAndAtomicInteger("900087");
    }

    @Benchmark
    public String nextByLocalDateTimeAndAtomicInteger() {
        return IdWork.nextByLocalDateTimeAndAtomicInteger("900087");
    }


    public static void main(String[] args) throws RunnerException {

        // 吞吐量
//        Options opt = new OptionsBuilder()
//                .include(IdTest.class.getSimpleName())
//                .mode(Mode.Throughput)
//                .forks(1)
//                .build();

        // 平均耗時
        Options opt = new OptionsBuilder()
                .include(IdTest.class.getSimpleName())
                .mode(Mode.AverageTime)
                .timeUnit(TimeUnit.NANOSECONDS)
                .forks(1)
                .build();

        new Runner(opt).run();

    }

// 吞吐量
//    Benchmark                                    Mode  Cnt        Score        Error  Units
//    IdTest.getIdBySnowflake                     thrpt    5  4070403.840 ±  11302.832  ops/s
//    IdTest.nextByCalendarAndAtomicInteger       thrpt    5  4201822.821 ± 177869.095  ops/s
//    IdTest.nextByCalendarAndRandom              thrpt    5  4085723.001 ±  47505.309  ops/s
//    IdTest.nextByLocalDateTimeAndAtomicInteger  thrpt    5  5036852.390 ± 153313.836  ops/s
//    IdTest.nextByLocalDateTimeAndRandom         thrpt    5  5199148.189 ± 405132.888  ops/s

// 平均耗時
//    Benchmark                                   Mode  Cnt    Score   Error  Units
//    IdTest.getIdBySnowflake                     avgt    5  245.739 ± 0.302  ns/op
//    IdTest.nextByCalendarAndAtomicInteger       avgt    5  239.174 ± 4.244  ns/op
//    IdTest.nextByCalendarAndRandom              avgt    5  251.084 ± 5.798  ns/op
//    IdTest.nextByLocalDateTimeAndAtomicInteger  avgt    5  197.332 ± 0.779  ns/op
//    IdTest.nextByLocalDateTimeAndRandom         avgt    5  212.105 ± 1.888  ns/op

}

概念理解

型別 作用域 描述 備註
Benchmark ElementType.METHOD 最重要的註解,標記需要執行的方法
BenchmarkMode ElementType.METHOD, ElementType.TYPE 統計的維度,有吞吐量,平均耗時,也可以組合使用
Fork ElementType.METHOD, ElementType.TYPE 複製多個程序來執行方法,每輪預設Iteration迴圈5次,如果fork 3,則會執行3*5 次,一般預設值1就可以
Measurement ElementType.METHOD, ElementType.TYPE 方法控制:迴圈次數,每次迴圈時間以及對應的時間單位
Warmup ElementType.METHOD,ElementType.TYPE 預熱,避免系統冷啟動導致的效能測試不準
OutputTimeUnit ElementType.METHOD, ElementType.TYPE 輸出時間單位,預設是秒
Param ElementType.FIELD 可以指定遍歷引數,針對特殊欄位測試不同的效能
Setup ElementType.METHOD 啟動類設定,類似 junit Before型別註解
TearDown ElementType.METHOD 銷燬類設定,類似junit After型別註解,一般用於銷燬池化的資源
Threads ElementType.METHOD,ElementType.TYPE
Timeout ElementType.METHOD,ElementType.TYPE
AuxCounters ElementType.TYPE 輔助計數器,可以統計 @State 修飾的物件中的 public 屬性被執行的情況
Group ElementType.METHOD
GroupThreads ElementType.METHOD
CompilerControl ElementType.METHOD, ElementType.CONSTRUCTOR, ElementType.TYPE 內聯擴充套件是一種特別的用於消除呼叫函數時所造成的固有時間消耗方法,這裡用來控制方法或類是否內聯
OperationsPerInvocation ElementType.METHOD, ElementType.TYPE

BenchmarkMode 執行模式(可以多個組合執行)

型別 描述
Throughput 每段時間執行的次數,一般是秒
AverageTime 平均時間,每次操作的平均耗時
SampleTime 在測試中,隨機進行取樣執行的時間
SingleShotTime 在每次執行中計算耗時
All 所有模式
// 常用的註解
@BenchmarkMode({Mode.Throughput,Mode.AverageTime})
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Warmup(iterations = 3, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Fork(1)
public class BenchmarkTest {
    @Benchmark
    public long test() {}
}

// 使用 OptionsBuilder 建造者模式構建 Options, 然後在main方法執行,建議使用
Options opt = new OptionsBuilder()
        .include(IdTest.class.getSimpleName())
        .mode(Mode.AverageTime)
        .mode(Mode.Throughput)
        .timeUnit(TimeUnit.NANOSECONDS)
        .warmupIterations(3)
        .warmupTime(TimeValue.seconds(1))
        .measurementIterations(5)
        .measurementTime(TimeValue.seconds(1))
        .forks(1)
        .build();

一些提示

避免迴圈

JVM會對迴圈進行優化,這樣會導致獲取的測試結果不準確。

參照資源

jmh-java-microbenchmark-harness
jenkov: java-performance
jmh-benchmark-with-examples
Java基準測試工具 —— JMH使用指南