慌!还不了解Java中的分支预测?!
點擊上方“朱小廝的博客”,選擇“設為星標”
后臺回復”1024“獲取公眾號專屬1024GB資料
來源:rrd.me/fLHvf
1.引言
分支預測是計算機科學中一個有趣的概念,它對我們的應用程序性能會產生深刻的影響。然而,這個概念通常沒有得到很好地理解,大多數開發者對此很少關注。
本文中,我們將探索分支預測的確切含義,如何對軟件產生影響,以及我們可以采取的行動。
2.什么是指令流水線?
開發計算機程序,本質上是編寫一組期望計算機順序執行的命令。
早期的計算機一次僅執行一條命令。這意味著每個命令都會加載到內存中,執行完成后再加載下一個命令。
指令流水線是一種改進。處理器會將工作分解成多個部分,對不同的部分并行執行。這樣,處理器能夠在加載下一條的同時執行一條命令。
處理器內部的指令流水線越長,不僅可以簡化還能并行執行更多的部分。這樣能夠提高系統的整體性能。
例如下面這個簡單的程序:
int a = 0; a += 1; a += 2; a += 3;程序會按照下面的流水線處理:Fetch(提取)、Decode(解碼)、Execute(執行)、Store(存儲):
這里可以看到四個命令如何并行處理,整體執行速度更快。
3.有什么危害?
處理器執行某些命令時會導致流水線問題。流水線中部分指令執行時依賴于之前的指令,但是前面的指令可能還沒有執行。
分支是一種危險。分支會挑選兩個執行方向之一執行,但只有在解析后才能確定是哪個方向。這意味著通過分支加載命令都是不安全的,因為無法知道從哪里加載命令。
修改上面的程序加入分支:
int a = 0; a += 1; if (a < 10) {a += 2; } a += 3;運行結果與之前相同,但其中加入了 if語句。在解析前,雖然計算機能看到這些指令,但不能加載 if 后面的指令。因此,執行的順序看起來像下面這樣:
現在可以立刻看到加入分支對程序執行造成的影響,得到相同結果所需的時鐘周期。
4.什么是分支預測?
分支預測是對上面的一種改進,計算機會嘗試預測分支的執行路徑,然后采取相應的動作。
在上面的示例中,處理器會預測if(a <10)為 true,因此把 a += 2 作為下一條待執行指令。這將導致執行的順序變成這樣:
可以看到程序的性能立即得到了提升:現在只要9個時鐘周期而不是之前的11個,速度提升了19%。
但是,這樣做也并非沒有風險。如果分支預測出錯,那么將對不應該執行的指令排隊。發生這種情況時,計算機要丟棄這些指令重新開始。
修改判斷條件改為false:
int a = 0; a += 1; if (a > 10) {a += 2; } a += 3;可能會像下面這樣執行:
現在,即使處理的指令更少,執行卻比之前慢!處理器錯誤地預測分支等于 true,把 a += 2 指令排隊。接著發現分支等于 false,必須丟棄已排隊的指令,然后重新執行。
5.對實際代碼的影響
現在我們知道分支預測的概念及優點,那么對實際代碼有什么影響?畢竟,這里討論的是在高速運行的計算機上損失幾個時鐘周期,影響肯定不會那么明顯。
有時候的確如此。但也有一些情況會對應用程序性能帶來顯著影響。很大程度上取決于進行的工作。具體來說,與在短時間內需要完成的工作有關。
5.1.統計列表條目
讓我們試著統計列表中的條目。接下來會生成一個數字列表,然后統計其中有多少個數字小于臨界值。與上面的示例類似,但是用循環取代了單個指令:
List<Long> numbers = LongStream.range(0, top).boxed().collect(Collectors.toList());if (shuffle) {Collections.shuffle(numbers); }long cutoff = top / 2; long count = 0;long start = System.currentTimeMillis(); for (Long number : numbers) {if (number < cutoff) {++count;} } long end = System.currentTimeMillis();LOG.info("Counted {}/{} {} numbers in {}ms",count, top, shuffle ? "shuffled": "sorted", end - start);請注意,這里只對計數循環計時,因為這才我們感興趣的工作。那么需要多少時間呢?
如果生成的列表較小,那么代碼執行很快,無法統計:10萬條數據的列表執行結果為0毫秒。但是當列表足夠大時,可以看到計時結果中 shuffle 條件對結果有顯著影響。包含1000萬個數字的列表:
Sorted – 44ms
Shuffled – 221ms
也就是說,即使計算的數據相同,隨機(shuffled)列表用時是排序后的5倍。
但是,排序操作本身的開銷比計數要大得多。我們應該分析自己的代碼,確定是否有性能提升的空間。
5.2.分支順序
從上面的內容可以看出,if/else 語句的分支順序很重要。也就是說,下面這樣的分支安排性能會更好:
if (mostLikely) {// Do something } else if (lessLikely) {// Do something } else if (leastLikely) {// Do something }然而,現代計算機可以通過分支預測緩存來避免這個問題。實際上,也可以對此進行測試:
List<Long> numbers = LongStream.range(0, top).boxed().collect(Collectors.toList()); if (shuffle) {Collections.shuffle(numbers); }long cutoff = (long)(top * cutoffPercentage); long low = 0; long high = 0;long start = System.currentTimeMillis(); for (Long number : numbers) {if (number < cutoff) {++low;} else {++high;} } long end = System.currentTimeMillis();LOG.info("Counted {}/{} numbers in {}ms", low, high, end - start);計算1000萬個數字時,無論 cutoffPercentage 如何設置,代碼執行的結果基本一致:排序數字約35毫秒,隨機數字約200毫秒。
這是因為分支預測器會平等地處理兩個分支,并正確猜測我們將采用其中哪一個。
5.3.合并條件
使用一個條件和兩個條件有什么區別?相同的結果可以用不同方式重寫實現邏輯,是否應該這樣做?
例如,可以用兩個數字分別與0比較,也可以用兩個數字的乘積與0比較。用乘法代替條件。這種做法是否值得?
讓我們考慮下面這個例子:
long[] first = LongStream.range(0, TOP).map(n -> Math.random() < FRACTION ? 0 : n).toArray(); long[] second = LongStream.range(0, TOP).map(n -> Math.random() < FRACTION ? 0 : n).toArray();long count = 0; long start = System.currentTimeMillis(); for (int i = 0; i < TOP; i++) {if (first[i] != 0 && second[i] != 0) {++count;} } long end = System.currentTimeMillis();LOG.info("Counted {}/{} numbers using separate mode in {}ms", count, TOP, end - start);像上面這樣,循環中的條件可以替換。實際上這樣做會影響結果:
分開條件– 40ms
單個條件(乘法)– 22ms
因此,使用兩個分開的條件實際的時間開銷是單個條件的兩倍。
6.總結
我們已經了解了什么是分支預測及其如何對程序產生影響。這樣我們又掌握了一個工具,保證程序盡可能高效。
盡管如此,在進行重大變更前,我們仍然需要一如既往地對代碼進行性能分析。有時不合理的分支條件可能會帶來更大的開銷。
本文的示例可以在GitHub上找到。
github.com/eugenp/tutorials/tree/master/core-java-modules/core-java-perf
想知道更多?掃描下面的二維碼關注我
【精彩推薦】
混沌工程初識
混沌工程的陷阱
Spring Boot 服務監控,健康檢查,線程信息,JVM堆信息,指標收集,運行情況監控等!
中國頂級互聯網公司的技術組織架構調整預示著什么?
朕已閱?
總結
以上是生活随笔為你收集整理的慌!还不了解Java中的分支预测?!的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 混沌工程的陷阱
- 下一篇: 干货!Java字节码增强探秘