[性能優(yōu)化一]不服跑個(gè)分-Java微基準(zhǔn)測(cè)試框架JMH

1. 為什么需要JMH

某些場(chǎng)景下需要精確地知道一段代碼的性能如何喉磁,如:

  1. 當(dāng)你已經(jīng)找出了熱點(diǎn)函數(shù)开缎,需要對(duì)熱點(diǎn)函數(shù)進(jìn)行進(jìn)一步優(yōu)化時(shí)粒褒;
  2. 想定量地知道某個(gè)函數(shù)需要執(zhí)行多長(zhǎng)時(shí)間,以及執(zhí)行時(shí)間和輸入 n 的相關(guān)性耿焊;
  3. 一個(gè)函數(shù)有兩種不同實(shí)現(xiàn)(例如JSON序列化/反序列化有Jackson和Gson實(shí)現(xiàn))揪惦,不知道哪種實(shí)現(xiàn)性能更好。

最簡(jiǎn)單的做法是在代碼執(zhí)行前后記錄下時(shí)間罗侯,然后計(jì)算一下時(shí)間差器腋,如:

long start = System.currentTimeMillis();
// 待測(cè)試的代碼塊...
System.out.println(System.currentTimeMillis() - start);

但是這樣做會(huì)有如下幾個(gè)問(wèn)題:

  1. System.currentTimeMillis() 如函數(shù)自身注釋所說(shuō),本身精度有限钩杰,根據(jù)操作系統(tǒng)不同纫塌,會(huì)存在數(shù)十毫秒左右的的誤差;
  2. JVM 在運(yùn)行時(shí)會(huì)進(jìn)行代碼預(yù)熱讲弄,說(shuō)白了就是越跑越快措左,因?yàn)轭?lèi)需要裝載、需要準(zhǔn)備操作垂睬;
  3. JVM 會(huì)在各個(gè)階段都有可能對(duì)代碼進(jìn)行優(yōu)化處理,比如某個(gè)計(jì)算的結(jié)果沒(méi)有被使用,那么這段代碼在執(zhí)行時(shí)就會(huì)被忽略驹饺,這樣的問(wèn)題比較難察覺(jué)钳枕;
  4. JVM垃圾回收的不確定性,可能運(yùn)行很快赏壹,回收很慢鱼炒;
  5. 使用不方便,配置不靈活蝌借,如果需要打印多種類(lèi)型的測(cè)試數(shù)據(jù)昔瞧,就需要增加很多額外的代碼,
    不容易修改測(cè)試的類(lèi)型和條件菩佑。

因此自晰,使用一款靠譜的benchmark工具,既可以減少工作量稍坯,又可以確保性能優(yōu)化過(guò)程不被錯(cuò)誤的測(cè)試數(shù)據(jù)誤導(dǎo)酬荞。

Java Microbenchmark Harness(JMH) 是由 Java 虛擬機(jī)團(tuán)隊(duì)開(kāi)發(fā)的一款用于 Java的微基準(zhǔn)測(cè)試工具,微基準(zhǔn)是指方法(method)層面的測(cè)試基準(zhǔn)瞧哟,精度可以精確到微秒級(jí)混巧。使用JMH 可以讓你方便快速地進(jìn)行一次嚴(yán)格的代碼基準(zhǔn)測(cè)試,并且有多種測(cè)試模式勤揩,多種測(cè)試維度可供選擇咧党。

2. JMH使用

JMH的使用可以參考官方示例 Code Sample ,本文則會(huì)介紹 JMH 最典型的用法和部分常用選項(xiàng)陨亡。

2.1 引入JMH依賴(lài)

在maven的配置文件中增加如下依賴(lài)傍衡,最新的依賴(lài)版本可以參考:
https://mvnrepository.com/artifact/org.openjdk.jmh/jmh-core
https://mvnrepository.com/artifact/org.openjdk.jmh/jmh-generator-annprocess

<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>

2.2 代碼示例

這里以Java String替換為例進(jìn)行說(shuō)明,問(wèn)題來(lái)源于Commons Lang StringUtils.replace performance vs String.replace数苫,示例代碼來(lái)自于java-str-benchmark聪舒,本文稍加改動(dòng),最終代碼如附錄1所示虐急。

對(duì)比的替換方式有:

  • String - JDK原生的字符串替換箱残;
  • StringUtils - Commons Lang的StringUtils.replace;
  • Lang3StringUtils - Commons Lang 3的StringUtils.replace止吁;
  • OSGL - OSGL Java tool (2.0.0-SNAPSHOT)被辑;
  • Fast - 自定義的快速字符串替換函數(shù)。

測(cè)試場(chǎng)景有:

  • Short text - 將AAAAAAAAAABBB中的AA替換成B敬惦;
  • Short text no match -將AAAAAAAAAABBB中的XYZ替換成B盼理;
  • Long text - 將長(zhǎng)文本中的occurrence替換成appearance
  • Long text no match - 將長(zhǎng)文本中的aaaxyz0001替換成appearance俄删。

2. 3. 執(zhí)行結(jié)果

measure item String StringUtils Lang3StringUtils OSGL Fast
short text 0.316 us/op 0.107 us/op 0.102 us/op 0.164 us/op 0.105 us/op
long text 14.492 us/op 6.860 us/op 11.324 us/op 9.887 us/op 7.005 us/op
short text no match 0.121 us/op 0.010 us/op 0.010 us/op 0.008 us/op 0.009 us/op
long text no match 3.008 us/op 2.298 us/op 2.319 us/op 1.302 us/op 3.359 us/op

附錄

附錄1 StringReplaceBenchmark.java

package com.liuil.core.benchmark;

import org.apache.commons.lang.StringUtils;
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;
import org.osgl.util.IO;
import org.osgl.util.S;

import java.util.concurrent.TimeUnit;


/**
 * ref:
 * https://stackoverflow.com/questions/16228992/commons-lang-stringutils-replace-performance-vs-string-replace
 * https://github.com/greenlaw110/java-str-benchmark
 */
@BenchmarkMode(Mode.AverageTime)
@State(Scope.Thread)
@Fork(1)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@Warmup(iterations = 5)
@Measurement(iterations = 5)
public class StringReplaceBenchmark {

    public static final String TGT_NO_MATCH = "XYZ";
    public static final String TGT = "AA";
    public static final String REPLACEMENT = "B";
    public static final String TEXT = "AAAAAAAAAABBB";
    public static final String TGT_NO_MATCH_LONG = "aaaxyz0001";
    public static final String TGT_LONG = "occurrence";
    public static final String REP_LONG = "appearance";
    public static final String TEXT_LONG = IO.readContentAsString(StringReplaceBenchmark.class.getResource("/long_str.txt"));

    @State(Scope.Thread)
    public static class BenchmarkState {
        volatile private String str = TEXT;
        volatile private String strLong = TEXT_LONG;
    }

    @Benchmark
    public Object testString(BenchmarkState state) {
        return state.str.replace(TGT, REPLACEMENT);
    }

    @Benchmark
    public Object testStringUtils(BenchmarkState state) {
        return StringUtils.replace(state.str, TGT, REPLACEMENT);
    }

    @Benchmark
    public Object testLang3StringUtils(BenchmarkState state) {
        return org.apache.commons.lang3.StringUtils.replace(state.str, TGT, REPLACEMENT);
    }

    @Benchmark
    public Object testOsgl(BenchmarkState state) {
        return S.have(state.str).replace(TGT).with(REPLACEMENT);
    }

    @Benchmark
    public Object testFast(BenchmarkState state) {
        return replace(state.str, TGT, REPLACEMENT);
    }

    @Benchmark
    public Object testStringNoMatch(BenchmarkState state) {
        return state.str.replace(TGT_NO_MATCH, REPLACEMENT);
    }

    @Benchmark
    public Object testStringUtilsNoMatch(BenchmarkState state) {
        return StringUtils.replace(state.str, TGT_NO_MATCH, REPLACEMENT);
    }

    @Benchmark
    public Object testLang3StringUtilsNoMatch(BenchmarkState state) {
        return org.apache.commons.lang3.StringUtils.replace(state.str, TGT_NO_MATCH, REPLACEMENT);
    }

    @Benchmark
    public Object testOsglNoMatch(BenchmarkState state) {
        return S.have(state.str).replace(TGT_NO_MATCH).with(REPLACEMENT);
    }

    @Benchmark
    public Object testFastNoMatch(BenchmarkState state) {
        return replace(state.str, TGT_NO_MATCH, REPLACEMENT);
    }

    @Benchmark
    public Object testStringLong(BenchmarkState state) {
        return state.strLong.replace(TGT_LONG, REP_LONG);
    }

    @Benchmark
    public Object testStringUtilsLong(BenchmarkState state) {
        return StringUtils.replace(state.strLong, TGT_LONG, REP_LONG);
    }

    @Benchmark
    public Object testLang3StringUtilsLong(BenchmarkState state) {
        return org.apache.commons.lang3.StringUtils.replace(state.strLong, TGT_LONG, REP_LONG);
    }

    @Benchmark
    public Object testOsglLong(BenchmarkState state) {
        return S.have(state.strLong).replace(TGT_LONG).with(REP_LONG);
    }

    @Benchmark
    public Object testFastLong(BenchmarkState state) {
        return replace(state.strLong, TGT_LONG, REP_LONG);
    }

    @Benchmark
    public Object testStringLongNoMatch(BenchmarkState state) {
        return state.strLong.replace(TGT_NO_MATCH_LONG, REPLACEMENT);
    }

    @Benchmark
    public Object testStringUtilsLongNoMatch(BenchmarkState state) {
        return StringUtils.replace(state.strLong, TGT_NO_MATCH_LONG, REPLACEMENT);
    }

    @Benchmark
    public Object testLang3StringUtilsLongNoMatch(BenchmarkState state) {
        return org.apache.commons.lang3.StringUtils.replace(state.strLong, TGT_NO_MATCH_LONG, REPLACEMENT);
    }

    @Benchmark
    public Object testOsglLongNoMatch(BenchmarkState state) {
        return S.have(state.strLong).replace(TGT_NO_MATCH_LONG).with(REPLACEMENT);
    }

    @Benchmark
    public Object testFastLongNoMatch(BenchmarkState state) {
        return replace(state.strLong, TGT_NO_MATCH_LONG, REPLACEMENT);
    }

    public static String replace(String source, String os, String ns) {
        if (source == null) {
            return null;
        }
        int i = 0;
        if ((i = source.indexOf(os, i)) >= 0) {
            char[] sourceArray = source.toCharArray();
            char[] nsArray = ns.toCharArray();
            int oLength = os.length();
            StringBuilder buf = new StringBuilder(sourceArray.length);
            buf.append(sourceArray, 0, i).append(nsArray);
            i += oLength;
            int j = i;
            // Replace all remaining instances of oldString with newString.
            while ((i = source.indexOf(os, i)) > 0) {
                buf.append(sourceArray, j, i - j).append(nsArray);
                i += oLength;
                j = i;
            }
            buf.append(sourceArray, j, sourceArray.length - j);
            source = buf.toString();
            buf.setLength(0);
        }
        return source;
    }


    public static void main(String[] args) throws RunnerException {
        Options opt = new OptionsBuilder().include(StringReplaceBenchmark.class.getSimpleName()).build();
        new Runner(opt).run();
    }

}


附錄2 執(zhí)行結(jié)果

Benchmark                                               Mode  Cnt   Score    Error  Units
StringReplaceBenchmark.testFast                         avgt    5   0.105 ±  0.012  us/op
StringReplaceBenchmark.testFastLong                     avgt    5   7.005 ±  0.170  us/op
StringReplaceBenchmark.testFastLongNoMatch              avgt    5   3.359 ±  0.064  us/op
StringReplaceBenchmark.testFastNoMatch                  avgt    5   0.009 ±  0.001  us/op
StringReplaceBenchmark.testLang3StringUtils             avgt    5   0.102 ±  0.002  us/op
StringReplaceBenchmark.testLang3StringUtilsLong         avgt    5  11.324 ±  0.183  us/op
StringReplaceBenchmark.testLang3StringUtilsLongNoMatch  avgt    5   2.319 ±  0.013  us/op
StringReplaceBenchmark.testLang3StringUtilsNoMatch      avgt    5   0.010 ±  0.001  us/op
StringReplaceBenchmark.testOsgl                         avgt    5   0.164 ±  0.004  us/op
StringReplaceBenchmark.testOsglLong                     avgt    5   9.887 ±  0.340  us/op
StringReplaceBenchmark.testOsglLongNoMatch              avgt    5   1.302 ±  0.031  us/op
StringReplaceBenchmark.testOsglNoMatch                  avgt    5   0.008 ±  0.002  us/op
StringReplaceBenchmark.testString                       avgt    5   0.316 ±  0.024  us/op
StringReplaceBenchmark.testStringLong                   avgt    5  14.492 ±  0.270  us/op
StringReplaceBenchmark.testStringLongNoMatch            avgt    5   3.008 ±  0.050  us/op
StringReplaceBenchmark.testStringNoMatch                avgt    5   0.121 ±  0.003  us/op
StringReplaceBenchmark.testStringUtils                  avgt    5   0.107 ±  0.002  us/op
StringReplaceBenchmark.testStringUtilsLong              avgt    5   6.860 ±  0.290  us/op
StringReplaceBenchmark.testStringUtilsLongNoMatch       avgt    5   2.298 ±  0.047  us/op
StringReplaceBenchmark.testStringUtilsNoMatch           avgt    5   0.010 ±  0.001  us/op

參考

  1. https://github.com/openjdk/jmh
  2. Java微基準(zhǔn)測(cè)試框架JMH
  3. Java benchmark 工具 JMH
  4. JMH - Java 代碼性能基準(zhǔn)測(cè)試
  5. https://stackoverflow.com/questions/16228992/commons-lang-stringutils-replace-performance-vs-string-replace
  6. https://github.com/greenlaw110/java-str-benchmark
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末宏怔,一起剝皮案震驚了整個(gè)濱河市奏路,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌臊诊,老刑警劉巖鸽粉,帶你破解...
    沈念sama閱讀 219,188評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異抓艳,居然都是意外死亡触机,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,464評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)玷或,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)儡首,“玉大人,你說(shuō)我怎么就攤上這事偏友∈呖瑁” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,562評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵约谈,是天一觀的道長(zhǎng)笔宿。 經(jīng)常有香客問(wèn)我,道長(zhǎng)棱诱,這世上最難降的妖魔是什么泼橘? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,893評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮迈勋,結(jié)果婚禮上炬灭,老公的妹妹穿的比我還像新娘。我一直安慰自己靡菇,他們只是感情好重归,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,917評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著厦凤,像睡著了一般鼻吮。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上较鼓,一...
    開(kāi)封第一講書(shū)人閱讀 51,708評(píng)論 1 305
  • 那天椎木,我揣著相機(jī)與錄音,去河邊找鬼博烂。 笑死香椎,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的禽篱。 我是一名探鬼主播畜伐,決...
    沈念sama閱讀 40,430評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼躺率!你這毒婦竟也來(lái)了玛界?” 一聲冷哼從身側(cè)響起万矾,我...
    開(kāi)封第一講書(shū)人閱讀 39,342評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎慎框,沒(méi)想到半個(gè)月后勤众,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,801評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡鲤脏,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,976評(píng)論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了吕朵。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片猎醇。...
    茶點(diǎn)故事閱讀 40,115評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖努溃,靈堂內(nèi)的尸體忽然破棺而出硫嘶,到底是詐尸還是另有隱情,我是刑警寧澤梧税,帶...
    沈念sama閱讀 35,804評(píng)論 5 346
  • 正文 年R本政府宣布沦疾,位于F島的核電站,受9級(jí)特大地震影響第队,放射性物質(zhì)發(fā)生泄漏哮塞。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,458評(píng)論 3 331
  • 文/蒙蒙 一凳谦、第九天 我趴在偏房一處隱蔽的房頂上張望忆畅。 院中可真熱鬧,春花似錦尸执、人聲如沸家凯。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,008評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)绊诲。三九已至,卻和暖如春褪贵,著一層夾襖步出監(jiān)牢的瞬間掂之,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,135評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工竭鞍, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留板惑,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,365評(píng)論 3 373
  • 正文 我出身青樓偎快,卻偏偏與公主長(zhǎng)得像冯乘,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子晒夹,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,055評(píng)論 2 355