摘要:處理器通過緩存能夠從數量級上降低內存延遲的成本這些緩存為了性能重新排列待定內存操作的順序。從上述觸發步驟中,可以看到第步發生了指令重排序,并導致第步讀到錯誤的數據。內存屏障是用來防止出現指令重排序的利器之一。
這兩天,我拜讀了 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指令緩存。我不知道該類型跟指令排序有什么關系,既然不在本文涉及范圍內,就不做深入探討了。
參考資料
http://kenwublog.com/docs/memory.barrier.ppt
http://kenwublog.com/docs/memory.model.instruction.reordering.and.store.atomicity.pdf
http://kenwublog.com/docs/memory.ordering.in.modern.microprocessor.pdf
http://en.wikipedia.org/wiki/Memory_ordering
http://en.wikipedia.org/wiki/Memory_Bank
via ifeve
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/69760.html
摘要:并發編程的挑戰并發編程的目的是為了讓程序運行的更快,但是,并不是啟動更多的線程就能讓程序最大限度的并發執行。的實現原理與應用在多線程并發編程中一直是元老級角色,很多人都會稱呼它為重量級鎖。 并發編程的挑戰 并發編程的目的是為了讓程序運行的更快,但是,并不是啟動更多的線程就能讓程序最大限度的并發執行。如果希望通過多線程執行任務讓程序運行的更快,會面臨非常多的挑戰:(1)上下文切換(2)死...
摘要:文章簡介分析的作用以及底層實現原理,這也是大公司喜歡問的問題內容導航的作用什么是可見性源碼分析的作用在多線程中,和都起到非常重要的作用,是通過加鎖來實現線程的安全性。而的主要作用是在多處理器開發中保證共享變量對于多線程的可見性。 文章簡介 分析volatile的作用以及底層實現原理,這也是大公司喜歡問的問題 內容導航 volatile的作用 什么是可見性 volatile源碼分析 ...
摘要:本文會先闡述在并發編程中解決的問題多線程可見性,然后再詳細講解原則本身。所以與內存之間的高速緩存就是導致線程可見性問題的一個原因。原則上面討論了中多線程共享變量的可見性問題及產生這種問題的原因。 Happens-Before是一個非常抽象的概念,然而它又是學習Java并發編程不可跨域的部分。本文會先闡述Happens-Before在并發編程中解決的問題——多線程可見性,然后再詳細講解H...
摘要:假設不發生編譯器重排和指令重排,線程修改了的值,但是修改以后,的值可能還沒有寫回到主存中,那么線程得到就是很自然的事了。同理,線程對于的賦值操作也可能沒有及時刷新到主存中。線程的最后操作與線程發現線程已經結束同步。 很久沒更新文章了,對隔三差五過來刷更新的讀者說聲抱歉。 關于 Java 并發也算是寫了好幾篇文章了,本文將介紹一些比較基礎的內容,注意,閱讀本文需要一定的并發基礎。 本文的...
閱讀 2458·2021-09-27 13:36
閱讀 2163·2019-08-29 18:47
閱讀 2129·2019-08-29 15:21
閱讀 1394·2019-08-29 11:14
閱讀 1979·2019-08-28 18:29
閱讀 1623·2019-08-28 18:04
閱讀 568·2019-08-26 13:58
閱讀 3206·2019-08-26 12:12