ThreadLocalRandom
類,它是Java 7中新增的用于生成隨機數的類。 我已在一系列微基準測試中分析了ThreadLocalRandom
的性能,以了解其在單線程環境中的性能。 結果相對令人驚訝:盡管代碼非常相似,但ThreadLocalRandom
速度是Math.random()
兩倍! 結果引起了我的興趣,我決定對此進行進一步的研究。 我已經記錄了我的分析過程。 它是對分析步驟,技術和一些JVM診斷工具的介紹,以了解小型代碼段的性能差異。 所描述的工具集和技術的一些經驗將使您能夠為特定的Hotspot目標環境編寫更快的Java代碼。
好,那就足夠了,讓我們開始吧! 我的機器是運行Windows XP的普通Intel 386 32位雙核。
Math.random()
處理Random
的靜態單例實例,而ThreadLocalRandom -> current() -> nextDouble()
處理ThreadLocalRandom
的線程本地實例,該實例是Random
的子類。 ThreadLocal
在每次調用current()
方法時引入了變量查找的開銷。 考慮到我剛才說的話,在單個線程中它的運行速度是Math.random()
的兩倍,這確實有點令人驚訝嗎? 我沒想到會有如此大的差異。
同樣,我使用的是Heinz博客之一中介紹的微型基準測試框架。 Heinz開發的框架解決了在現代JVM上對Java程序進行基準測試時遇到的一些挑戰。 這些挑戰包括:熱身,垃圾回收,Javas time API的準確性,測試準確性的驗證等等。
這是我可運行的基準測試類:
public class ThreadLocalRandomGenerator implements BenchmarkRunnable {private double r;@Overridepublic void run() {r = r + ThreadLocalRandom.current().nextDouble();}public double getR() {return r;}@Overridepublic Object getResult() {return r;}}public class MathRandomGenerator implements BenchmarkRunnable {private double r;@Overridepublic void run() {r = r + Math.random();}public double getR() {return r;}@Overridepublic Object getResult() {return r;}
}
讓我們使用Heinz的框架運行基準測試:
public class FirstBenchmark {private static List<BenchmarkRunnable> benchmarkTargets = Arrays.asList(new MathRandomGenerator(),new ThreadLocalRandomGenerator());public static void main(String[] args) {DecimalFormat df = new DecimalFormat("#.##");for (BenchmarkRunnable runnable : benchmarkTargets) {Average average = new PerformanceHarness().calculatePerf(new PerformanceChecker(1000, runnable), 5);System.out.println("Benchmark target: " + runnable.getClass().getSimpleName());System.out.println("Mean execution count: " + df.format(average.mean()));System.out.println("Standard deviation: " + df.format(average.stddev()));System.out.println("To avoid dead code coptimization: " + runnable.getResult());}}
}
注意:為了確保JVM不會將代碼標識為“死代碼”,我返回了一個字段變量,并立即打印出基準測試的結果。 這就是為什么我的可運行類實現名為RunnableBenchmark的接口。 我已經運行了三次基準測試。 第一次運行是在默認模式下,啟用了內聯和JIT優化:
Benchmark target: MathRandomGenerator
Mean execution count: 14773594,4
Standard deviation: 180484,9
To avoid dead code coptimization: 6.4005410634212025E7
Benchmark target: ThreadLocalRandomGenerator
Mean execution count: 29861911,6
Standard deviation: 723934,46
To avoid dead code coptimization: 1.0155096190946539E8
然后再次不進行JIT優化(VM選項-Xint
):
Benchmark target: MathRandomGenerator
Mean execution count: 963226,2
Standard deviation: 5009,28
To avoid dead code coptimization: 3296912.509302683
Benchmark target: ThreadLocalRandomGenerator
Mean execution count: 1093147,4
Standard deviation: 491,15
To avoid dead code coptimization: 3811259.7334526842
最后一個測試是使用JIT優化,但是使用-XX:MaxInlineSize=0
,它(幾乎)禁用了內聯:
Benchmark target: MathRandomGenerator
Mean execution count: 13789245
Standard deviation: 200390,59
To avoid dead code coptimization: 4.802723374491231E7
Benchmark target: ThreadLocalRandomGenerator
Mean execution count: 24009159,8
Standard deviation: 149222,7
To avoid dead code coptimization: 8.378231170741305E7
讓我們仔細地解釋結果:借助完整的JVM JIT優化, ThreadLocalRanom
速度是Math.random()
兩倍。 關閉JIT優化表明,兩者的性能相同(差)。 方法內聯似乎使性能相差30%。 其他差異可能歸因于其他優化技術 。
JIT編譯器可以更有效地調整ThreadLocalRandom
原因之一是ThreadLocalRandom.next()
的改進實現。
public class Random implements java.io.Serializable {
...protected int next(int bits) {long oldseed, nextseed;AtomicLong seed = this.seed;do {oldseed = seed.get();nextseed = (oldseed * multiplier + addend) & mask;} while (!seed.compareAndSet(oldseed, nextseed));return (int)(nextseed >>> (48 - bits));}
...
}public class ThreadLocalRandom extends Random {
...protected int next(int bits) {rnd = (rnd * multiplier + addend) & mask;return (int) (rnd >>> (48-bits));}
...
}
第一個片段顯示Random.next()
,它在Math.random()
的基準測試中大量使用。 與ThreadLocalRandom.next()
相比,該方法需要更多的指令,盡管這兩種方法都做同樣的事情。 在Random
類中, seed
變量將全局共享狀態存儲到所有線程,并且每次調用next()
方法時都會更改。 因此,需要AtomicLong
安全地訪問和更改對nextDouble()
調用中的seed
值。 另一方面, ThreadLocalRandom
是–很好–線程局部:-) next()
方法不必是線程安全的,可以使用普通的long
變量作為種子值。
關于方法內聯和ThreadLocalRandom
方法內聯是一種非常有效的JIT優化。 在頻繁執行的熱路徑中,熱點編譯器決定將被調用方法(子方法)的代碼內聯到調用方方法(父方法)中。 內聯具有重要的好處。 它顯著降低了方法調用的動態頻率,從而節省了執行這些方法調用所需的時間。 但更重要的是,內聯會產生更大的代碼塊,以供優化程序使用。 這就造成了一種情況,大大提高了傳統編譯器優化的效率,克服了提高Java編程語言性能的主要障礙。”
從Java 7開始,您可以使用診斷JVM選項監視方法內聯。 使用' -XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining
'運行代碼將顯示JIT編譯器的內聯工作。 以下是Math.random()
基準測試輸出的相關部分:
@ 13 java.util.Random::nextDouble (24 bytes)@ 3 java.util.Random::next (47 bytes) callee is too large@ 13 java.util.Random::next (47 bytes) callee is too large
JIT編譯器無法內聯Random.next()
中調用的Random.nextDouble()
。 這是ThreaLocalRandom.next()
的內聯輸出:
@ 8 java.util.Random::nextDouble (24 bytes)@ 3 java.util.concurrent.ThreadLocalRandom::next (31 bytes)@ 13 java.util.concurrent.ThreadLocalRandom::next (31 bytes)
由于next()
方法較短(31個字節),因此可以內聯它。 因為在兩個基準測試中都強烈調用next()
方法,所以該日志表明方法內聯可能是ThreadLocalRandom
顯著提高執行速度的原因之一。
為了驗證這一點并查找更多信息,需要深入研究匯編代碼。 使用Java 7 JDK,可以將匯編代碼打印到控制臺中。 有關如何啟用-XX:+PrintAssembly
VM選項的信息,請參見此處 。 該選項將打印出JIT優化的代碼,這意味著您可以看到JVM實際執行的代碼。 我已經將相關的匯編代碼復制到下面的鏈接中。
此處的ThreadLocalRandomGenerator.run()的匯編代碼。
MathRandomGenerator.run()的匯編代碼在此處 。
Math.random() 在此處調用的Random.next()的匯編代碼。
匯編代碼是機器特定的低級代碼,比字節代碼要復雜得多。 讓我們嘗試在我的基準測試中驗證方法內聯對性能的影響,以及:JIT編譯器如何處理ThreadLocalRandom
和Math.random
()還有其他明顯的區別嗎? 在ThreadLocalRandomGenerator.run()
,沒有對任何子例程(如Random.nextDouble()
或ThreatLocalRandom.next()
過程調用。 僅可見一個虛擬(因此很昂貴)的ThreadLocal.get()
)方法調用(請參閱ThreadLocalRandomGenerator.run()
程序集的第35行)。 其他所有代碼都內聯到ThreadLocalRandomGenerator.run()
。 在的情況下MathRandomGenerator.run()
有兩個虛擬方法調用到Random.next()
見塊B4線204頁及以后中的匯編代碼MathRandomGenerator.run()
這一事實證實了我們的懷疑,即方法內聯是導致性能差異的一個重要根本原因。 此外,由于同步的麻煩, Random.next()
需要的匯編指令要多得多(并且有些昂貴!),這在執行速度方面也適得其反。
了解invokevirtual
指令的開銷
那么,為什么(虛擬)方法調用昂貴且方法內聯如此有效? invokevirtual
指令的指針不是類實例中具體方法的偏移量。 編譯器不知道類實例的內部布局。 相反,它生成對實例方法的符號引用,這些符號引用存儲在運行時常量池中。 這些運行時常量池項將在運行時解析,以確定實際的方法位置。 這種動態(運行時)綁定需要驗證,準備和解決,??這可能會大大影響性能。 (有關詳細信息,請參見JVM規范中的調用方法和鏈接 )。
目前為止就這樣了。 免責聲明:當然,解決性能難題需要了解的主題列表無窮無盡。 除了微基準測試,JIT優化,方法內聯,java字節碼,assemby語言等等之外,還有更多的知識要理解。 同樣,除了虛擬方法調用或昂貴的線程同步指令之外,還有更多導致性能差異的根本原因。 但是,我認為我所介紹的主題是此類深入研究的一個好的開始。 期待批評和愉快的評論!
參考資料:來自JCG合作伙伴 Niklas的“ Java 7:如何編寫真正快速的Java代碼”。
翻譯自: https://www.javacodegeeks.com/2012/01/java-7-how-to-write-really-fast-java.html