說來也是巧最近在看 Dubbo 源碼,而後發現了一處很奇怪的代碼,恰好和這個 switch 和 if else 有關!程序員
讓咱們來看一下這段代碼,它屬於 ChannelEventRunnable,這個 runnable 是 Dubbo IO 線程建立,將此任務扔到業務線程池中處理,整理了一份近期Java+最多見的+200+學習筆記+面試題彙總,但願對你們的吸取有幫助。面試
看到沒,把 state == ChannelState.RECEIVED 拎出來獨立一個 if,而其餘的 state 仍是放在 switch 裏面判斷。數組
我當時腦子裏就來回掃描,想一想這個到底有什麼花頭,奈何知識淺薄一臉懵逼。安全
因而就開始了一波探險之旅!ide
遇到問題固然是問搜索引擎了,通常而言我會同時搜索各大引擎,咱這也不說誰比誰好,反正有些時候度娘仍是不錯的,好比此次搜索度娘給的結果比較靠前,google 比較靠後。性能
通常搜索東西我都喜歡先在官網上搜,找不到了再放開搜,因此先這麼搜 site:xxx.com key。學習
你看這就有了,完美啊!測試
咱們先來看看官網的這篇博客怎麼說的,而後再詳細地分析一波。優化
現代 CPU 都支持分支預測 (branch prediction) 和指令流水線 (instruction pipeline),這兩個結合能夠極大提升 CPU 效率。對於簡單的 if 跳轉,CPU 是能夠比較好地作分支預測的。可是對於 switch 跳轉,CPU 則沒有太多的辦法。 switch 本質上是根據索引,從地址數組裏取地址再跳轉。搜索引擎
也就是說 if 是跳轉指令,若是是簡單的跳轉指令的話 CPU 能夠利用分支預測來預執行指令,而 switch 是要先根據值去一個相似數組結構找到對應的地址,而後再進行跳轉,這樣的話 CPU 預測就幫不上忙了。
而後又由於一個 channel 創建了以後,超過99.9%狀況它的 state 都是 ChannelState.RECEIVED,所以就把這個狀態給挑出來,這樣就能利用 CPU 分支預測機制來提升代碼的執行效率。
而且還給出了 Benchmark 的代碼,就是經過隨機生成 100W 個 state,而且 99.99% 是 ChannelState.RECEIVED,而後按照如下兩種方式來比一比(這 benchSwitch 官網的例子名字打錯了,我一開始沒發現後來校對文章才發現)。
雖然博客也給出了它的對比結果,可是我仍是本地來跑一下看看結果如何,其實 JMH 不推薦在 ide 裏面跑,可是我懶,直接 idea 裏面跑了。
從結果來看確實經過 if 獨立出來代碼的執行效率更高(注意這裏測的是吞吐),博客還提出了這種技巧能夠放在性能要求嚴格的地方,也就是通常狀況下不必這樣特殊作。
至此咱們已經知道了這個結論是對的,不過咱們還須要深刻分析一波,首先得看看 if 和 switch 的執行方式到底差異在哪裏,而後再看看 CPU 分支預測和指令流水線的究竟是幹啥的,爲何會有這兩個東西?
咱們先簡單來個小 demo 看看 if 和 switch 的執行效率,其實就是添加一個所有是 if else 控制的代碼, switch 和 if + switch 的不動,看看它們之間對比效率如何(此時仍是 RECEIVED 超過99.9%)。
來看一下執行的結果如何:
好傢伙,我跑了好幾回,這全 if 的比 if + switch 強很多啊,因此是否是源碼應該全改爲 if else 的方式,你看這吞吐量又高,還不會像如今一下 if 一下又 switch 有點不三不四的樣子。
我又把 state 生成的值改爲隨機的,再來跑一下看看結果如何:
我跑了屢次仍是 if 的吞吐量都是最高的,怎麼整這個全 if 的都是最棒滴。
在個人印象裏這個 switch 應該是優於 if 的,不考慮 CPU 分支預測的話,單從字節碼角度來講是這樣的,咱們來看看各自生成的字節碼。
先看一下 switch 的反編譯,就截取了關鍵部分。
也就是說 switch 生成了一個 tableswitch,上面的 getstatic 拿到值以後能夠根據索引直接查這個 table,而後跳轉到對應的行執行便可,也就是時間複雜度是 O(1)。
好比值是 1 那麼直接跳到執行 64 行,若是是 4 就直接跳到 100 行。
關於 switch 還有一些小細節,當 swtich 內的值不連續且差距很大的時候,生成的是 lookupswitch,按網上的說法是二分法進行查詢(我沒去驗證過),時間複雜度是 O(logn),不是根據索引直接能找到了,我看生成的 lookup 的樣子應該就是二分了,由於按值大小排序了。
還有當 switch 裏面的值不連續可是差距比較小的時候,仍是會生成 tableswtich 不過填充了一些值,好比這個例子我 switch 裏面的值就 一、三、五、七、9,它自動填充了二、四、六、8 都指到 default 所跳的行。
讓咱們再來看看 if 的反編譯結果:
能夠看到 if 是每次都會取出變量和條件進行比較,而 switch 則是取一次變量以後查表直接跳到正確的行,從這方面來看 switch 的效率應該是優於 if 的。固然若是 if 在第一次判斷就過了的話也就直接 goto 了,不會再執行下面的哪些判斷了。
因此從生成的字節碼角度來看 switch 效率應該是大於 if 的,可是從測試結果的角度來看 if 的效率又是高於 switch 的,不管是隨機生成 state,仍是 99.99% 都是同一個 state 的狀況下。
首先 CPU 分支預測的優化是確定的,那關於隨機狀況下 if 仍是優於 switch 的話這我就有點不太肯定爲何了,多是 JIT 作了什麼優化操做,或者是隨機狀況下分支預測成功帶來的效益大於預測失敗的情形?
難道是我枚舉值太少了體現不出 switch 的效果?不過在隨機狀況下 switch 也不該該弱於 if 啊,我又加了 7 個枚舉值,一共 12 個值又測試了一遍,結果以下:
好像距離被拉近了,我看有戲,因而我背了波 26 個字母,實不相瞞仍是唱着打的字母。
擴充了分支的數量後又進行了一波測試,此次 swtich 爭氣了,終於比 if 強了。
題外話: 我看網上也有對比 if 和 switch 的,它們對比出來的結果是 switch 優於 if,首先 jmh 就沒寫對,定義一個常量來測試 if 和 switch,而且測試方法的 result 寫了沒有消費,這代碼也不知道會被 JIT 優化成啥樣了,寫了幾十行,可能直接優化成 return 某個值了。
對比了這麼多咱們來小結一下。
首先對於熱點分支將其從 switch 提取出來用 if 獨立判斷,充分利用 CPU 分支預測帶來的便利確實優於純 swtich,從咱們的代碼測試結果來看,大體吞吐量高了兩倍。
而在熱點分支的情形下改爲純 if 判斷而不是 if + swtich的情形下,吞吐量提升的更多。是純 switch 的 3.3 倍,是 if + switch 的 1.6 倍。
在隨機分支的情形下,三者差異不是很大,可是仍是純 if 的狀況最優秀。
可是從字節碼角度來看其實 switch 的機制效率應該更高的,不管是 O(1) 仍是 O(logn),可是從測試結果的角度來講不是的。
在選擇條件少的狀況下 if 是優於 switch 的,這個我不太清楚爲何,多是在值較少的狀況下查表的消耗相比帶來的收益更大一些?有知道的小夥伴能夠在文末留言。
在選擇條件不少的狀況下 switch 是優於 if 的,再多的選擇值我就沒測了,大夥有興趣能夠本身測測,不過趨勢就是這樣的。
接下來我們再來看看這個分支預測究竟是怎麼弄的,爲何會有分支預測這玩意,不過在談到分支預測以前須要先介紹下指令流水線(Instruction pipelining),也就是現代微處理器的 pipeline。
CPU 本質就是取指執行,而取指執行咱們來看下五大步驟,分別是獲取指令(IF)、指令解碼(ID)、執行指令(EX)、內存訪問(MEM)、寫回結果(WB),再來看下維基百科上的一個圖。
固然步驟實際可能更多,反正就是這個意思須要經歷這麼多步,因此說一次執行能夠分紅不少步驟,那麼這麼多步驟就能夠並行,來提高處理的效率。
因此說指令流水線就是試圖用一些指令使處理器的每一部分保持忙碌,方法是將傳入的指令分紅一系列連續的步驟,由不一樣的處理器單元執行,不一樣的指令部分並行處理。
就像咱們工廠的流水線同樣,我這個奧特曼的腳拼上去了立刻拼下一個奧特曼的腳,我可不會等上一個奧特曼的都組裝完了再組裝下一個奧特曼。
固然也沒有這麼死板,不必定就是順序執行,有些指令在等待然後面的指令其實不依賴前面的結果,因此能夠提早執行,這種叫亂序執行。
咱們再說回咱們的分支預測。
這代碼就像咱們的人生同樣總會面臨着選擇,只有作了選擇以後才知道後面的路怎麼走呀,可是事實上發現這代碼常常走的是同一個選擇,因而就想出了一個分支預測器,讓它來預測走勢,提早執行一路的指令。
那預測錯了怎麼辦?這和我們人生不同,它能夠把以前執行的結果全拋了而後再來一遍,可是也有影響,也就是流水線越深,錯的越多浪費的也就越多,錯誤的預測延遲是10至20個時鐘週期之間,因此仍是有反作用的。
簡單的說就是經過分支預測器來預測未來要跳轉執行的那些指令,而後預執行,這樣到真正須要它的時候能夠直接拿到結果了,提高了效率。
分支預測又分了不少種預測方式,有靜態預測、動態預測、隨機預測等等,從維基百科上看有16種。
我簡單說下我提到的三種,靜態預測就是愣頭青,就和蒙英語選擇題同樣,我管你什麼題我都選A,也就是說它會預測一個走勢,勇往直前,簡單粗暴。
動態預測則會根據歷史記錄來決定預測的方向,好比前面幾回選擇都是 true ,那我就走 true 要執行的這些指令,若是變了最近幾回都是 false ,那我就變成 false 要執行的這些指令,其實也是利用了局部性原理。
隨機預測看名字就知道了,這是蒙英語選擇題的另外一種方式,瞎猜,隨機選一個方向直接執行。
還有不少就不一一列舉了,各位有興趣自行去研究,順便提一下在 2018 年穀歌的零項目和其餘研究人員公佈了一個名爲 Spectre 的災難性安全漏洞,其可利用 CPU 的分支預測執行泄漏敏感信息,這裏就不展開了,文末會附上連接。
以後又有個名爲 BranchScope 的***,也是利用預測執行,因此說每當一個新的玩意出來老是會帶來利弊。
至此咱們已經知曉了什麼叫指令流水線和分支預測了,也理解了 Dubbo 爲何要這麼優化了,可是文章尚未結束,我還想提一提這個 stackoverflow 很是有名的問題,看看這數量。
這個問題在那篇博客開頭就被提出來了,很明顯這也是和分支預測有關係,既然看到了索性就再分析一波,大夥能夠在腦海裏先回答一下這個問題,畢竟我們都知道答案了,看看思路清晰不。
就是下面這段代碼,數組排序了以後循環的更快。
而後各路大神就蹦出來了,咱們來看一下首讚的大佬怎麼說的。
一開口就是,直擊要害。
You are a victim of branch prediction fail.
緊接着就上圖了,一看就是老司機。
他說讓咱們回到 19世紀,一個沒法遠距離交流且無線電還未普及的時候,若是是你這個鐵路交叉口的扳道工,當火車快來的時候,你如何得知該扳哪一邊?
火車停車再重啓的消耗是很大的,每次到分叉口都停車,而後你問他,哥們去哪啊,而後扳了道,再重啓就很耗時,怎麼辦?猜!
猜對了火車就不用停,繼續開。猜錯了就停車而後倒車而後換道再開。
因此就看猜的準不許了!搏一搏單車變摩托。
而後大佬又指出了關鍵代碼對應的彙編代碼,也就是跳轉指令了,這對應的就是火車的岔口,該選條路了。
後面我就不分析了,大夥兒應該都知道了,排完序的數組執行到值大於 128 的以後確定所有大於128了,因此每次分支預測的結果都是對了!因此執行的效率很高。
而沒排序的數組是亂序的,因此不少時候都會預測錯誤,而預測錯誤就得指令流水線排空啊,而後再來一遍,這速度固然就慢了。
因此大佬說這個題主你是分支預測錯誤的受害者。
最終大佬給出的修改方案是咱不用 if 了,惹不起咱還躲不起嘛?直接利用位運算來實現這個功能,具體我就不分析了,給你們看下大佬的建議修改方案。
這篇文章就差很少了,今天就是從 Dubbo 的一段代碼開始了探險之旅,分析了波 if 和 switch,從測試結果來看 Dubbo 的此次優化還不夠完全,應該所有改爲 if else 結構。
而 swtich 從字節碼上看是優於 if 的,可是從測試結果來看在分支不少的狀況下能顯示出優點,通常狀況下仍是打不過 if 。
而後也知曉了什麼叫指令流水線,這其實就是結合實際了,流水線纔夠快呀,而後分支預測預執行也是一個提升效率的方法,固然得猜的對,否則分支預測錯誤的反作用仍是沒法忽略的,因此對分支預測器的要求也是很高的。