從JVM併發看CPU記憶體指令重排序(Memory Reordering)
文中提到:
對主存的一次訪問一般花費硬體的數百次時鐘週期。處理器通過快取(caching)能夠從數量級上降低記憶體延遲的成本這些快取為了效能重新排列待定記憶體操作的順序。也就是說,程式的讀寫操作不一定會按照它要求處理器的順序執行。
這段話是作者對記憶體屏障重要性的定義。通過cache降低記憶體延遲,這句話很好理解。但後面那句“為了效能重排序記憶體操作順序”,讓沒學好微機原理的我倍感疑惑。
CPU為何要重排序記憶體訪問指令?在哪種場景下會觸發重排序?作者在文中並未提及。
為了解答疑問,我在網上查閱了一些資料,在這裡跟大家分享一下。
重排序的背景
我們知道現代CPU的主頻越來越高,與cache的互動次數也越來越多。當CPU的計算速度遠遠超過訪問cache時,會產生cache wait,過多的cache ?wait就會造成效能瓶頸。
針對這種情況,多數架構(包括X86)採用了一種將cache分片的解決方案,即將一塊cache劃分成互不關聯地多個 slots (邏輯儲存單元,又名
Memory Bank的劃分
一般 Memory bank 是按cache address來劃分的。比如 偶數adress 0×12345000?分到 bank 0, 奇數address 0×12345100?分到 bank1。
重排序的種類
編譯期重排。編譯原始碼時,編譯器依據對上下文的分析,對指令進行重排序,以之更適合於CPU的並行執行。
執行期重排,CPU在執行過程中,動態分析依賴部件的效能,對指令做重排序優化。
例項講解指令重排序原理
為了方便理解,我們先來看一張CPU內部結構圖。
從圖中可以看到,這是一臺配備雙CPU的計算機,cache 按地址被分成了兩塊 cache banks,分別是?cache bank0 和 cache bank1。
理想的記憶體訪問指令順序:
1,CPU0往?cache address 0×12345000 寫入一個數字 1。因為address 0×12345000是偶數,所以值被寫入 bank0.
2,CPU1讀取 bank0 address 0×12345000 的值,即數字1。
3,CPU0往 cache 地址 0×12345100 ?寫入一個數字 2。因為address 0×12345100是奇數,所以值被寫入 bank1.
4,CPU1讀取 bank1 address ?0×12345100 的值,即數字2。
重排序後的記憶體訪問指令順序:
1,CPU0 準備往 bank0 address 0×12345000 寫入數字 1。
2,CPU0檢查 bank0 的可用性。發現 bank0 處於 busy 狀態。
3, CPU0 為了防止 cache等待,發揮最大效能,將記憶體訪問指令重排序。即先執行後面的 bank1 address 0×12345100 數字2的寫入請求。
4,CPU0檢查 bank1 可用性,發現bank1處於 idle 狀態。
5,CPU0 將數字2寫入 bank 1 address 0×12345100。
6,CPU1來讀取 ?0×12345000,未讀到 數字1,出錯。
7, CPU0 繼續檢查 bank0 的可用性,發現這次?bank0 可用了,然後將數字1寫入 0×12345000。
8, CPU1 讀取 0×12345100,讀到數字2,正確。
從上述觸發步驟中,可以看到第 3 步發生了指令重排序,並導致第 6步讀到錯誤的資料。
通過對指令重排,CPU可以獲得更快地響應速度,但也給編寫併發程式的程式設計師帶來了諸多挑戰。
記憶體屏障是用來防止CPU出現指令重排序的利器之一。
通過這個例項,不知道你對指令重排理解了沒有?
不同架構下的指令重排優化
從圖中,可以看到,X86僅在 Stores after loads 和 Incoherent instruction cache pipeline 中會觸發重排。
Stores after loads的含義是在對同一個地址進行讀寫操作時,寫入在讀取後面,允許重排序。即滿足弱一致性(Weak Consistency),這是最可被接受的型別,不會造成太大的影響。
Incoherent instruction cache pipeline是跟JIT相關的型別,作用是在執行self-modifying code 時預防JIT沒有flush指令快取。我不知道該型別跟指令排序有什麼關係,既然不在本文涉及範圍內,就不做深入探討了。
參考資料