閱讀61 返回首頁    go 阿裏雲 go 技術社區[雲棲]


從JVM並發看CPU內存指令重排序(Memory Reordering)

這兩天,我拜讀了 Dennis Byrne 寫的一片博文Memory Barriers and JVM Concurrency (中譯文內存屏障與JVM並發)。

文中提到:

對主存的一次訪問一般花費硬件的數百次時鍾周期。處理器通過緩存(caching)能夠從數量級上降低內存延遲的成本這些緩存為了性能重新排列待定內存操作的順序。也就是說,程序的讀寫操作不一定會按照它要求處理器的順序執行。

這段話是作者對內存屏障重要性的定義。通過cache降低內存延遲,這句話很好理解。但後麵那句“為了性能重排序內存操作順序”,讓沒學好微機原理的我倍感疑惑。

CPU為何要重排序內存訪問指令?在哪種場景下會觸發重排序?作者在文中並未提及。

為了解答疑問,我在網上查閱了一些資料,在這裏跟大家分享一下。

 

重排序的背景

我們知道現代CPU的主頻越來越高,與cache的交互次數也越來越多。當CPU的計算速度遠遠超過訪問cache時,會產生cache wait,過多的cache ?wait就會造成性能瓶頸。
針對這種情況,多數架構(包括X86)采用了一種將cache分片的解決方案,即將一塊cache劃分成互不關聯地多個 slots (邏輯存儲單元,又名 Memory Bank 或 Cache Bank),CPU可以自行選擇在多個 idle bank 中進行存取。這種 SMP 的設計,顯著提高了CPU的並行處理能力,也回避了cache訪問瓶頸。

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指令緩存。我不知道該類型跟指令排序有什麼關係,既然不在本文涉及範圍內,就不做深入探討了。

參考資料

https://kenwublog.com/docs/memory.barrier.ppt
https://kenwublog.com/docs/memory.model.instruction.reordering.and.store.atomicity.pdf
https://kenwublog.com/docs/memory.ordering.in.modern.microprocessor.pdf
https://en.wikipedia.org/wiki/Memory_ordering
https://en.wikipedia.org/wiki/Memory_Bank


文章轉自 並發編程網-ifeve.com

最後更新:2017-05-22 17:01:34

  上一篇:go  Java NIO係列教程(七) FileChannel
  下一篇:go  Java NIO係列教程(一) Java NIO 概述