JAVA拾遺 — JMH與8個測試陷阱

前言

JMH 是 Java Microbenchmark Harness(微基準測試)框架的縮寫(2013年首次發佈)。與其餘衆多測試框架相比,其特點優點在於它是由 Oracle 實現 JIT 的相同人員開發的。在此,我想特別提一下 Aleksey Shipilev (JMH 的做者兼佈道者)和他優秀的博客文章。筆者花費了一個週末,將 Aleksey 大神的博客,特別是那些和 JMH 相關的文章通讀了幾遍,外加一部公開課視頻 《"The Lesser of Two Evils" Story》 ,將本身的收穫概括在這篇文章中,文中很多圖片都來自 Aleksey 公開課視頻。java

閱讀本文前

本文沒有花費專門的篇幅在文中介紹 JMH 的語法,若是你使用過 JMH,那固然最好,但若是沒聽過它,也不須要擔憂(跟我一週前的狀態同樣)。我會從 Java Developer 角度來談談一些常見的代碼測試陷阱,分析他們和操做系統底層以及 Java 底層的關聯性,並藉助 JMH 來幫助你們擺脫這些陷阱。git

通讀本文,須要一些操做系統相關以及部分 JIT 的基礎知識,若是遇到陌生的知識點,能夠留意章節中的維基百科連接,以及筆者推薦的博客。程序員

筆者能力有限,未能徹底理解 JMH 解決的所有問題,若有錯誤以及疏漏歡迎留言與我交流。github

初識 JMH

測試精度

測試精度

上圖給出了不一樣類型測試的耗時數量級,能夠發現 JMH 能夠達到微秒級別的的精度。算法

這樣幾個數量級的測試所面臨的挑戰也是不一樣的。shell

  • 毫秒級別的測試並非很困難
  • 微秒級別的測試是具有挑戰性的,但並不是沒法完成,JMH 就作到了
  • 納秒級別的測試,目前尚未辦法精準測試
  • 皮秒級別…Holy Shit

圖解:數組

Linpack : Linpack benchmark 一類基礎測試,度量系統的浮點計算能力緩存

SPEC:Standard Performance Evaluation Corporation 工業界的測試標準組織微信

pipelining:系統總線通訊的耗時多線程

Benchmark 分類

測試在不一樣的維度能夠分爲不少類:集成測試,單元測試,API 測試,壓力測試… 而 Benchmark 一般譯爲基準測試(性能測試)。你能夠在不少開源框架的包層級中發現 Benchmark,用於闡釋該框架的基準水平,從而量化其性能。

基準測試又能夠細分爲 :Micro benchmark,Kernels,Synthetic benchmark,Application benchmarks.etc.本文的主角便屬於 Benchmark 的 Micro benchmark。基礎測試分類詳細介紹 here

motan中的benchmark

爲何須要有 Benchmark

If you cannot measure it, you cannot improve it.

--Lord Kelvin

俗話說,沒有實踐就沒有發言權,Benchmark 爲應用提供了數據支持,是評價和比較方法好壞的基準,Benchmark 的準確性,多樣性便顯得尤其重要。

Benchmark 做爲應用框架,產品的基準畫像,存在統一的標準,避免了不一樣測評對象自說自話的尷尬,應用框架各自使用有利於自身場景的測評方式必然不可取,例如 Standard Performance Evaluation Corporation (SPEC) 即上文「測試精度」提到的詞即是工業界的標準組織之一,JMH 的做者 Aleksey 也是其中的成員。

JMH 長這樣

@Benchmark
public void measure() {
    // this method was intentionally left blank.
}
複製代碼

使用起來和單元測試同樣的簡單

它的測評結果

Benchmark                                Mode  Cnt           Score           Error  Units
JMHSample_HelloWorld.measure  thrpt    5  3126699413.430 ± 179167212.838  ops/s
複製代碼

爲何須要 JMH 測試

你可能會想,我用下面的方式來測試有什麼很差?

long start = System.currentTimeMillis();
measure();
System.out.println(System.currentTimeMillis()-start);
複製代碼

難道 JMH 不是這麼測試的嗎?

@Benchmark
public void measure() {
}
複製代碼

事實上,這是本文的核心問題,建議在閱讀時時刻帶着這樣的疑問,爲何不使用第一種方式來測試。在下面的章節中,我將列舉諸多的測試陷阱,他們都會爲這個問題提供論據,這些陷阱會啓發那些對「測試」不感冒的開發者。

預熱

在初識 JMH 小節的最後,花少許的篇幅來給 JMH 涉及的知識點開個頭,介紹一個 Java 測試中比較老生常談的話題 — 預熱(warm up),它存在於下面全部的測試中。

«Warmup» = waiting for the transient responses to settle down

特別是在編寫 Java 測試程序時,預熱歷來都是不可或缺的一環,它使得結果更加真實可信。

warmup plateaus

上圖展現了一個樣例測評程序隨着迭代次數增多執行耗時變化的曲線,能夠發如今 120 次迭代以後,性能才趨於最終穩定,這意味着:預熱階段須要有至少 120 次迭代,才能獲得準確的基礎測試報告。(JVM 初始化時的一些準備工做以及 JIT 優化是主要緣由,但不是惟一緣由)。須要被說明的事,JMH 的運行相對耗時,由於,預熱被前置在每個測評任務以前。

使用 JMH 解決 12 個測試陷阱

陷阱1:死碼消除

死碼消除

measureWrong 方法想要測試 Math.log 的性能,獲得的結果和空方法 baseline 一致,而 measureRight 相比 measureWrong 多了一個 return,正確的獲得了測試結果。

這是因爲 JIT 擅長刪除「無效」的代碼,這給咱們的測試帶來了一些意外,當你意識到 DCE 現象後,應當有意識的去消費掉這些孤立的代碼,例如 return。JMH 不會自動實施對冗餘代碼的消除。

死碼消除這個概念不少人其實並不陌生,註釋的代碼,不可達的代碼塊,可達但不被使用的代碼等等,我這裏補充一些 Aleksey 提到的概念,用以闡釋爲什麼通常測試方法難以免引用對象發生死碼消除現象:

  1. Fast object combinator.
  2. Need to escape object to limit thread-local optimizations.
  3. Publishing the object ⇒ reference heap write ⇒ store barrier.

很絕望,我的水平有限,我沒能 get 到這些點,只能原封不動地貼給你們看了。

JMH 提供了專門的 API — Blockhole 來避免死碼消除問題。

@Benchmark
public void measureRight(Blackhole bh) {
    bh.consume(Math.log(PI));
}
複製代碼

陷阱2:常量摺疊與常量傳播

常量摺疊 (Constant folding) 是一個在編譯時期簡化常數的一個過程,常數在表示式中僅僅表明一個簡單的數值,就像是整數 2,如果一個變數從未被修改也可做爲常數,或者直接將一個變數被明確地被標註爲常數,例以下面的描述:

i = 320 * 200 * 32;
複製代碼

多數的現代編譯器不會真的產生兩個乘法的指令再將結果儲存下來,取而代之的,他們會辨識出語句的結構,並在編譯時期將數值計算出來(在這個例子,結果爲 2,048,000)。

有些編譯器,常數摺疊會在初期就處理完,例如 Java 中的 final 關鍵字修飾的變量就會被特殊處理。而將常數摺疊放在較後期的階段的編譯器,也至關常見。

private double x = Math.PI;

// 編譯器會對 final 變量特殊處理 
private final double wrongX = Math.PI;

@Benchmark
public double baseline() { // 2.220 ± 0.352 ns/op
    return Math.PI;
}

@Benchmark
public double measureWrong_1() { // 2.220 ± 0.352 ns/op
    // 錯誤,結果能夠被預測,會發生常量摺疊
    return Math.log(Math.PI);
}

@Benchmark
public double measureWrong_2() { // 2.220 ± 0.352 ns/op
    // 錯誤,結果能夠被預測,會發生常量摺疊
    return Math.log(wrongX);
}

@Benchmark
public double measureRight() { // 22.590 ± 2.636 ns/op
    return Math.log(x);
}
複製代碼

通過 JMH 能夠驗證這一點:只有最後的 measureRight 正確測試出了 Math.log 的性能,measureWrong_1,measureWrong_2 都受到了常量摺疊的影響。

常數傳播(Constant propagation) 是一個替表明示式中已知常數的過程,也是在編譯時期進行,包含前述所定義,內建函數也適用於常數,如下列描述爲例:

int x = 14;
  int y = 7 - x / 2;
  return y * (28 / x + 2);
複製代碼

傳播能夠理解變量的替換,若是進行持續傳播,上式會變成:

int x = 14;
  int y = 0;
  return 0;
複製代碼

陷阱3:永遠不要在測試中寫循環

這個陷阱對咱們作平常測試時的影響也是巨大的,因此我直接將他做爲了標題:永遠不要在測試中寫循環!

本節設計很多知識點,循環展開(loop unrolling),JIT & OSR 對循環的優化。對於前者循環展開的定義,建議讀者直接查看 wiki 的定義,而對於後者 JIT & OSR 對循環的優化,推薦兩篇 R 大的知乎回答:

循環長度的相同、循環體代碼相同的兩次for循環的執行時間相差了100倍?

OSR(On-Stack Replacement)是怎樣的機制?

對於第一個回答,建議不要看問題,直接看答案;第二個回答,闡釋了 OSR 都對循環作了哪些手腳。

測試一個耗時較短的方法,入門級程序員(不瞭解動態編譯的同窗)會這樣寫,經過循環放大,再求均值。

public class BadMicrobenchmark {
    public static void main(String[] args) {
        long startTime = System.nanoTime();
        for (int i = 0; i < 10_000_000; i++) {
            reps();
        }
        long endTime = System.nanoTime();
        System.out.println("ns/op : " + (endTime - startTime));
    }
}
複製代碼

實際上,這段代碼的結果是不可預測的,太多影響因子會干擾結果。原理暫時不表,經過 JMH 來看看幾個測試方法,下面的 Benchmark 嘗試對 reps 方法迭代不一樣的次數,想從中得到 reps 真實的性能。(注意,在 JMH 中使用循環也是不可取的,除非你是 Benchmark 方面的專家,不然在任什麼時候候,你都不該該寫循環)

int x = 1;
int y = 2;

@Benchmark
public int measureRight() {
    return (x + y);
}

private int reps(int reps) {
    int s = 0;
    for (int i = 0; i < reps; i++) {
        s += (x + y);
    }
    return s;
}

@Benchmark
@OperationsPerInvocation(1)
public int measureWrong_1() {
    return reps(1);
}

@Benchmark
@OperationsPerInvocation(10)
public int measureWrong_10() {
    return reps(10);
}

@Benchmark
@OperationsPerInvocation(100)
public int measureWrong_100() {
    return reps(100);
}

@Benchmark
@OperationsPerInvocation(1000)
public int measureWrong_1000() {
    return reps(1000);
}

@Benchmark
@OperationsPerInvocation(10000)
public int measureWrong_10000() {
    return reps(10000);
}

@Benchmark
@OperationsPerInvocation(100000)
public int measureWrong_100000() {
    return reps(100000);
}
複製代碼

結果以下:

Benchmark                               Mode  Cnt  Score   Error  Units
JMHSample_11_Loops.measureRight         avgt    5  2.343 ± 0.199  ns/op
JMHSample_11_Loops.measureWrong_1       avgt    5  2.358 ± 0.166  ns/op
JMHSample_11_Loops.measureWrong_10      avgt    5  0.326 ± 0.354  ns/op
JMHSample_11_Loops.measureWrong_100     avgt    5  0.032 ± 0.011  ns/op
JMHSample_11_Loops.measureWrong_1000    avgt    5  0.025 ± 0.002  ns/op
JMHSample_11_Loops.measureWrong_10000   avgt    5  0.022 ± 0.005  ns/op
JMHSample_11_Loops.measureWrong_100000  avgt    5  0.019 ± 0.001  ns/op
複製代碼

若是不看事先給出的錯誤和正確的提示,上述的結果,你會選擇相信哪個?實際上跑分耗時從 2.358 隨着迭代次數變大,降爲了 0.019。手動測試循環的代碼 BadMicrobenchmark 也存在一樣的問題,實際上它沒有作預熱,效果只會比 JMH 測試循環更加不可信。

Aleksey 在視頻中給出結論:假設單詞迭代的耗時是 𝑀 ns. 在 JIT,OSR,循環展開等因素的多重做用下,屢次迭代的耗時理論值爲 𝛼𝑀 ns, 其中 𝛼 ∈ [0; +∞)。

正確的測試循環的姿式能夠看這裏:here

陷阱4:使用 Fork 隔離多個測試方法

相信我,這個陷阱中涉及到的例子絕對是 JMH sample 中最詭異的,而且我尚未找到科學的解釋(說實話視頻中這一段我嘗試聽了好幾遍,沒聽懂,原諒個人聽力)

首先定義一個 Counter 接口,並實現了兩份代碼徹底相同的實現類:Counter1,Counter2

public interface Counter {
    int inc();
}

public class Counter1 implements Counter {
    private int x;

    @Override
    public int inc() {
        return x++;
    }
}

public class Counter2 implements Counter {
    private int x;

    @Override
    public int inc() {
        return x++;
    }
}
複製代碼

接着讓他們在同一個 VM 中按照先手順序進行評測:

public int measure(Counter c) {
    int s = 0;
    for (int i = 0; i < 10; i++) {
        s += c.inc();
    }
    return s;
}

/* * These are two counters. */
Counter c1 = new Counter1();
Counter c2 = new Counter2();

/* * We first measure the Counter1 alone... * Fork(0) helps to run in the same JVM. */
@Benchmark
@Fork(0)
public int measure_1_c1() {
    return measure(c1);
}

/* * Then Counter2... */
@Benchmark
@Fork(0)
public int measure_2_c2() {
    return measure(c1);
}

/* * Then Counter1 again... */
@Benchmark
@Fork(0)
public int measure_3_c1_again() {
    return measure(c1);
}

@Benchmark
@Fork(1)
public int measure_4_forked_c1() {
    return measure(c1);
}

@Benchmark
@Fork(1)
public int measure_5_forked_c2() {
    return measure(c2);
}
複製代碼

這一個例子中多了一個 Fork 註解,讓我來簡單介紹下它。Fork 這個關鍵字顧名思義,是用來將運行環境複製一份的意思,在咱們以前的多個測試中,實際上每次測評都是默認使用了相互隔離的,徹底一致的測評環境,這得益於 JMH。每一個試驗運行在單獨的 JVM 進程中。也能夠指定(額外的) JVM 參數,例如這裏爲了演示運行在同一個 JVM 中的弊端,特意作了反面的教材:Fork(0)。試想一下 c1,c2,c1 again 的耗時結果會如何?

Benchmark                                 Mode  Cnt   Score   Error  Units
JMHSample_12_Forking.measure_1_c1         avgt    5   2.518 ± 0.622  ns/op
JMHSample_12_Forking.measure_2_c2         avgt    5  14.080 ± 0.283  ns/op
JMHSample_12_Forking.measure_3_c1_again   avgt    5  13.462 ± 0.164  ns/op
JMHSample_12_Forking.measure_4_forked_c1  avgt    5   3.861 ± 0.712  ns/op
JMHSample_12_Forking.measure_5_forked_c2  avgt    5   3.574 ± 0.220  ns/op
複製代碼

你會不會感到驚訝,第一次運行的 c1 居然耗時最低,在個人認知中,JIT 起碼會啓動預熱的做用,不管如何都不可能先運行的方法比以後的方法快這麼多!但這個結果也和 Aleksey 視頻中介紹的相符。

JMH samples 中的這個示例主要仍是想要表達同一個 JVM 中運行的測評代碼會互相影響,從結果也能夠發現:c1,c2,c1_again 的實現相同,跑分卻不一樣,由於運行在同一個 JVM 中;而 forked_c1 和 forked_c2 則表現出了一致的性能。因此沒有特殊緣由,Fork 的值通常都須要設置爲 >0。

陷阱5:方法內聯

熟悉 C/C++ 的朋友不會對方法內聯感到陌生,方法內聯就是把目標方法的代碼「複製」到發起調用的方法之中,避免發生真實的方法調用(減小了操做指令週期)。在 Java 中,沒法手動編寫內聯方法,但 JVM 會自動識別熱點方法,並對它們使用方法內聯優化。一段代碼須要執行多少次纔會觸發 JIT 優化一般這個值由 -XX:CompileThreshold 參數進行設置:

  • 一、使用 client 編譯器時,默認爲1500;
  • 二、使用 server 編譯器時,默認爲10000;

可是一個方法就算被 JVM 標註成爲熱點方法,JVM 仍然不必定會對它作方法內聯優化。其中有個比較常見的緣由就是這個方法體太大了,分爲兩種狀況。

  • 若是方法是常常執行的,默認狀況下,方法大小小於 325 字節的都會進行內聯(能夠經過-XX:MaxFreqInlineSize=N來設置這個大小)
  • 若是方法不是常常執行的,默認狀況下,方法大小小於 35 字節纔會進行內聯(能夠經過-XX:MaxInlineSize=N來設置這個大小)

咱們能夠經過增長這個大小,以便更多的方法能夠進行內聯;可是除非可以顯著提高性能,不然不推薦修改這個參數。由於更大的方法體會致使代碼內存佔用更多,更少的熱點方法會被緩存,最終的效果不必定好。

若是想要知道方法被內聯的狀況,可使用下面的JVM參數來配置

-XX:+PrintCompilation //在控制檯打印編譯過程信息
-XX:+UnlockDiagnosticVMOptions //解鎖對JVM進行診斷的選項參數。默認是關閉的,開啓後支持一些特定參數對JVM進行診斷
-XX:+PrintInlining //將內聯方法打印出來
複製代碼

方法內聯的其餘隱含條件

  • 雖然 JIT 號稱能夠針對代碼全局的運行狀況而優化,可是 JIT 對一個方法內聯以後,仍是可能由於方法被繼承,致使須要類型檢查而沒有達到性能的效果
  • 想要對熱點的方法使用上內聯的優化方法,最好儘可能使用final、private、static這些修飾符修飾方法,避免方法由於繼承,致使須要額外的類型檢查,而出現效果很差狀況。

方法內聯也可能對 Benchmark 產生影響;或者說有時候咱們爲了優化代碼,而故意觸發內聯,也能夠經過 JMH 來和非內聯方法進行性能對比:

public void target_blank() {
    // this method was intentionally left blank
}

@CompilerControl(CompilerControl.Mode.DONT_INLINE)
public void target_dontInline() {
    // this method was intentionally left blank
}

@CompilerControl(CompilerControl.Mode.INLINE)
public void target_inline() {
    // this method was intentionally left blank
}
複製代碼
Benchmark                                Mode  Cnt   Score    Error  Units
JMHSample_16_CompilerControl.blank       avgt    3   0.323 ±  0.544  ns/op
JMHSample_16_CompilerControl.dontinline  avgt    3   2.099 ±  7.515  ns/op
JMHSample_16_CompilerControl.inline      avgt    3   0.308 ±  0.264  ns/op
複製代碼

能夠發現,內聯與不內聯的性能差距是巨大的,有一些空間換時間的味道,在 JMH 中使用 CompilerControl.Mode 來控制內聯是否開啓。

陷阱6:僞共享與緩存行

又遇到了咱們的老朋友:CPU Cache 和緩存行填充。這個併發性能殺手,我在以前的文章中專門介紹過,若是你沒有看過,能夠戳這裏:JAVA 拾遺 — CPU Cache 與緩存行。在 Benchmark 中,有時也不能忽視緩存行對測評的影響。

受限於篇幅,在此不展開有關僞共享的陷阱,完整的測評能夠戳這裏:JMHSample_22_FalseSharing

JMH 爲解決僞共享問題,提供了 @State 註解,但並不能在單一對象內部對個別的字段增長,若是有必要,可使用併發包中的 @Contended 註解來處理。

Aleksey 曾爲 Java 併發包提供過優化,其中就包括 @Contended 註解。

陷阱7:分支預測

分支預測(Branch Prediction)是這篇文章中介紹的最後一個 Benchmark 中的「搗蛋鬼」。仍是從一個具體的 Benchmark 中觀察結果。下面的代碼嘗試遍歷了兩個長度相等的數組,一個有序,一個無序,並在迭代時加入了一個判斷語句,這是分支預測的關鍵:if(v > 0)

private static final int COUNT = 1024 * 1024;

private byte[] sorted;
private byte[] unsorted;

@Setup
public void setup() {
    sorted = new byte[COUNT];
    unsorted = new byte[COUNT];
    Random random = new Random(1234);
    random.nextBytes(sorted);
    random.nextBytes(unsorted);
    Arrays.sort(sorted);
}

@Benchmark
@OperationsPerInvocation(COUNT)
public void sorted(Blackhole bh1, Blackhole bh2) {
    for (byte v : sorted) {
        if (v > 0) { //關鍵
            bh1.consume(v);
        } else {
            bh2.consume(v);
        }
    }
}

@Benchmark
@OperationsPerInvocation(COUNT)
public void unsorted(Blackhole bh1, Blackhole bh2) {
    for (byte v : unsorted) {
        if (v > 0) { //關鍵
            bh1.consume(v);
        } else {
            bh2.consume(v);
        }
    }
}
複製代碼
Benchmark                               Mode  Cnt  Score   Error  Units
JMHSample_36_BranchPrediction.sorted    avgt   25  2.752 ± 0.154  ns/op
JMHSample_36_BranchPrediction.unsorted  avgt   25  8.175 ± 0.883  ns/op
複製代碼

從結果看,有序數組的遍歷比無序數組的遍歷快了 2-3 倍。關於這點的介紹,最佳的解釋來自於 Stack Overflow 一個 2w 多讚的答案:Why is it faster to process a sorted array than an unsorted array?

分叉路口

假設咱們是在 19 世紀,而你負責爲火車選擇一個方向,那時連電話和手機尚未普及,當火車開來時,你不知道火車往哪一個方向開。因而你的作法(算法)是:叫停火車,此時火車停下來,你去問司機,而後你肯定了火車往哪一個方向開,並把鐵軌扳到了對應的軌道。

還有一個須要注意的地方是,火車的慣性是很是大的,因此司機必須在很遠的地方就開始減速。當你把鐵軌扳正確方向後,火車從啓動到加速又要通過很長的時間。

那麼是否有更好的方式能夠減小火車的等待時間呢?

有一個很是簡單的方式,你提早把軌道扳到某一個方向。那麼到底要扳到哪一個方向呢,你使用的手段是——「瞎蒙」:

  • 若是蒙對了,火車直接經過,耗時爲 0。
  • 若是蒙錯了,火車中止,而後倒回去,你將鐵軌扳至反方向,火車從新啓動,加速,行駛。

若是你很幸運,每次都蒙對了,火車將從不停車,一直前行!若是不幸你蒙錯了,那麼將浪費很長的時間。

雖然不嚴謹,但你能夠用一樣的道理去揣測 CPU 的分支預測,有序數組使得這樣的預測大部分狀況下是正確的,因此帶有判斷條件時,有序數組的遍歷要比無序數組要快。

這同時也啓發咱們:在大規模循環邏輯中要儘可能避免大量判斷(是否是能夠抽取到循環外呢?)。

陷阱8:多線程測試

多線程測試

在 4 核的系統之上運行一個測試方法,獲得如上的測試結果, Ops/nsec 表明了單位時間內的運行次數,Scale 表明 2,4 線程相比 1 線程的運行次數倍率。

這個圖可供咱們提出兩個問題:

  1. 爲何 2 線程 -> 4 線程幾乎沒有變化?
  2. 爲何 2 線程相比 1 線程只有 1.87 倍的變化,而不是 2 倍?

1 電源管理

降頻

第一個影響因素即是多線程測試會受到操做系統電源管理(Power Management)的影響,許多系統存在能耗和性能的優化管理。 (Ex: cpufreq, SpeedStep, Cool&Quiet, TurboBoost)

當咱們主動對機器進行降頻以後,總體性能發生降低,可是 Scale 在線程數 1 -> 2 的過程當中變成了嚴謹的 2 倍。

這樣的問題並不是沒法規避,補救方法即是禁用電源管理, 保證 CPU 的時鐘頻率 。

JMH 經過長時間運行,保證線程不出現 park(time waiting) 狀態,來保證測試的精準性。

2 操做系統調度和分時調用模型

形成多線程測試陷阱的第二個問題,須要從線程調度模型出發來理解:分時調度模型和搶佔式調度模型。

分時調度模型是指讓全部的線程輪流得到 CPU 的使用權,而且平均分配每一個線程佔用的 CPU 的時間片,這個也比較好理解;搶佔式調度模型,是指優先讓可運行池中優先級高的線程佔用 CPU,若是可運行池中的線程優先級相同,那麼就隨機選擇一個線程,使其佔用 CPU。處於運行狀態的線程會一直運行,直至它不得不放棄 CPU。一個線程會由於如下緣由而放棄 CPU。

須要注意的是,線程的調度不是跨平臺的,它不只僅取決於 Java 虛擬機,還依賴於操做系統。在某些操做系統中,只要運行中的線程沒有遇到阻塞,就不會放棄 CPU;在某些操做系統中,即便線程沒有遇到阻塞,也會運行一段時間後放棄 CPU,給其它線程運行的機會。

不管是那種模型,線程上下文的切換都會形成損耗。到這兒爲止,仍是隻回答了第一個問題:爲何 2 線程相比 1 線程只有 1.87 倍的變化,而不是 2 倍?

因爲上述的兩個圖我都是從 Aleksey 的視頻中摳出來的,並不清楚他的實際測試用例,對於 2 -> 4 線程性能差距並不大隻能理解爲系統過載,按道理說 4 核的機器,運行 4 個線程應該不至於只比 2 個線程快這麼一點。

對於線程分時調用以及線程調度帶來的不穩定性,JMH 引入了 bogus iterations 的概念,它保障了在多線程測試過程當中,只在線程處於忙碌狀態的過程當中進行測量。

bogus iterations

bogus iterations 這個值得一提,我理解爲「僞迭代」,而且也只在 JVM 的註釋以及 Aleksey 的幾個博客中有介紹,能夠理解爲 JMH 的內部原理的專用詞。

總結

本文花了大量的篇幅介紹了 JMH 存在的意義,以及 JMH sample 中提到的諸多陷阱,這些陷阱會很是容易地被那些不規範的測評程序所觸發。我以爲做爲 Java 語言的使用者,起碼有必要了解這些現象的存在,畢竟 JMH 已經幫你解決了諸多問題了,你不用擔憂預熱問題,不用本身寫比較 low 的循環去評測,規避這些測試陷阱也變得相對容易。

實際上,本文設計的知識點,僅僅是 Aleksey 博客中的內容、 JMH 的 38 個 sample 的冰山一角,有興趣的朋友能夠戳這裏查看全部的 JMH sample

陷阱心裏 os:像我這麼diao的陷阱,還有 30 個!

kafka

例如 Kafka 這樣優秀的開源框架,提供了專門的 module 來作 JMH 的基礎測試。嘗試使用 JMH 做爲你的 Benchmark 工具吧。

歡迎關注個人微信公衆號:「Kirito的技術分享」,關於文章的任何疑問都會獲得回覆,帶來更多 Java 相關的技術分享。

關注微信公衆號
相關文章
相關標籤/搜索