C++的std::atomic與體系結構中多核內存模型/緩存一致性協議(監聽 目錄協議)有什麼關係?

既然硬體上有緩存一致性協議(cache-coherency protocol 為什麼還需要std:atomic volatile之流? x86的緩存一致性協議是怎麼做的?

相關緩存一致性協議大致有:MESIF/MESI.

補充一下。可能有幾個子問題:

  1. 在不同核上的多個線程操作讀/寫同一個內存地址,假設指令是有序執行的,x86架構下是否保證了CPU緩存一致性?即某個核上的線程寫該地址會使其他核上該塊內存的緩存換出?
  2. std:atomic 與 volatile內存柵欄(asm volatile("" ::: "memory");)似乎和緩存一致性協議功能上重複了
  3. 寄存器是否涉及緩存協議?
  4. 指令重排發生於:編譯器?CPU優化?


atomic解決的是「我改的時候我知道別人也在改」的這麼個事情,volatile解決的則是「我讀的時候是真的在讀」,這都跟緩存一致性沒有什麼關係。


首先得保證編譯器正確地把你的代碼翻譯成你想要的能保證緩存一致性的那個樣子


要看memory model,look this

https://github.com/GHScan/TechNotes/blob/master/2017/Memory_Model.md


MESIF/MESI 這類協議確實是強一致的印象。但是現代處理器一般給每個核的 MESI 外麵包一個 Store Buffer、Invalidate Queue 之類的東西,多核訪問共享內存不顯式加 Memory Barrier 指令就有可見性問題了。內存柵欄指令用於確保內存訪問的可見性。不過 x86 的 Memory Model 挺強的,印象中只有 StoreLoad 一個亂序,所以內存柵欄指令不怎麼多(印象中鎖了匯流排就有柵欄語義?)。

給編譯器看的 barrier 是避免編譯器重排,編譯器重排了指令,內存柵欄指令的位置變了,也是亂的。流水線冒險那個 『亂序執行(out of order execution)』 跟內存的 『亂序訪問』 不是一回事,感覺流水線冒險階段的指令重排跟這塊關係不怎麼大,CPU 總不會對柵欄指令做亂序執行的對吧...

了解內存柵欄看這個最好:Memory Barriers: a Hardware View for Software Hackers https://pdfs.semanticscholar.org/da5d/eb60d4d48039e63e527c06e7bd7face6597c.pdf


緩存一致性是硬體乾的活。跟這裡沒關係。除了一些高性能計算的處理器,比如GPU和SW26010會有軟體可控的緩存以外,其它情況下緩存100%不影響程序邏輯(隻影響性能)。

關於C++的問題,看例子

typedef double elem_t;

elem_t func1(elem_t *p) {
return *p + *p;
}

elem_t func2(volatile elem_t *p) {
return *p + *p;
}

elem_t func3(std::atomic& *p) {
return p-&>load(std::memory_order_relaxed) + p-&>load(std::memory_order_relaxed);
}

elem_t func4(std::atomic& *p) {
return p-&>load(std::memory_order_seq_cst) + p-&>load(std::memory_order_seq_cst);
}

編譯器clang,%rcx是參數寄存器。

第一個,明明寫了兩個*p卻只讀了一次內存。編譯器默認兩次讀出來的是相同的數據

movsd (%rcx), %xmm0
addsd %xmm0, %xmm0
retq

第二個,加volatile,這次知道要讀兩次了。因為讀兩次之間有可能被其它線程修改,導致兩次讀到的不一樣

movsd (%rcx), %xmm0
addsd (%rcx), %xmm0
retq

第三個和第四個的彙編我沒太讀懂,只能強行解釋。

第三個,用atomic,不限內存順序。因為讀一次的中間過程可能被其它線程修改。比如先讀前4位元組,被其他線程修改,再讀後4位元組,那麼讀到的數據可能是壞的。這種情況下需要用保證一次讀完的指令。強行解釋:movsd和movq的區別?

movq (%rcx), %xmm1
movq (%rcx), %xmm0
addsd %xmm1, %xmm0
retq

第四個,有內存順序。雖然兩條movq指令中,%xmm1的那條指令在前面,但是處理器可不保證分先後執行,它在認為可以先執行後一條的時候就可能先執行後一條。假設有個其它線程在不斷地給*p+=xxx,那麼處理器的亂序執行無法保證%xmm1一定不大於%xmm0。為此需要加內存屏障。

強行解釋一下:兩條movq的目標寄存器都是同一個寄存器%rax,處理器看到這兩條的時候一想壞了,兩條指令之間有數據依賴,不能亂序執行。

movq (%rcx), %rax
movq %rax, %xmm1
movq (%rcx), %rax
movq %rax, %xmm0
addsd %xmm1, %xmm0
retq


硬體是硬體,軟體是軟體。要分開討論。這是兩個不同層次的緩存同步。

volatile是阻止編譯器優化。編譯器是怎麼優化的呢,就是可能會把內存某一個數放到寄存器里。這時如果某一個線程修改了內存里的數據,那麼寄存器里的緩存是感知不到的,你的程序就出了問題。那麼實際上這裡解決的是我們軟體層面的緩存同步問題(寄存器--內存)。

緩存一致性解決的是內存跟多核cpu緩存之間的同步問題,cpu緩存大部分時間對你的程序都是透明的,我們軟體總以為自己操作的是內存,但是實際上在硬體上,CPU是不能直接訪問內存的,它是通過多段緩存層層通信才訪問到的內存。現代CPU往往都是多核的,每個核都有自己緩存,如果某一個核的緩存對應的內存被修改了,另一個核怎麼才能知道呢,因為這個時候這個核的緩存還是舊的值,緩存一致性其實解決的是這個硬體層面的緩存同步問題(內存---CPU緩存)。

這樣一來整個流程你就明白了

其實我們寄存器---內存同步流程是要依賴硬體的緩存一致性協議的,因為CPU實際上訪問的是緩存,而不是真正的內存,只不過這個部分變成了透明的,讓軟體覺得自己好像是在操作內存罷了。


atomic int i = 0; 的意思是如果N個線程執行++i 這樣的操作這樣的操作的話,那麼她們的所有操作結果一定不重不漏的覆蓋了所有的整數(在i重新變成0之前)。

換句話說shared_ptr的引用計數是原子的,這保證無論有多少個線程操作引用計數,一定有且只有一個線程能把計數從非0值減少為0,進而發起對對象的刪除操作(絕不會發生重複刪除,或者泄漏)。

volatile 是一些編譯器優化上的事情,可以理解為一種特殊的「局部O0"。

總之都和硬體體系結構沒啥關係呢。


看你的提問,感覺你有三個很大的問題:

1. 把 cache coherence 與 memory ordering 的概念完全搞混了

2. 把 cache coherence 和 volatile 工作的層級完全搞混了

3. 把 寄存器 跟 CPU cache 搞混了

就簡單點說吧,memory ordering 決定『指令』訪問內存(包括 load 和 store 操作)的順序。內存亂序(memory reordering)可以發生在編譯時,稱為 software memory reordering,即編譯器為了效率將訪存指令重排(這個跟 cache coherence 毛關係沒有);也可以發生在執行時,被稱為 hardware memory reordering,即由 CPU 體系結構引起的指令執行時亂序。你所說的代碼 asm volatile("" ::: "memory") 只是告訴編譯器不要重排指令序而已,也就是為了避免 software memory reordering;

cache coherence 協議,例如 MESI 是針對 CPU cache 而言的,其伴隨著 CPU cache 的出現而出現,由 CPU 硬體保證實現的。直觀點說,多個 CPU 都從自己 cache 讀取數據的時候,cache肯定會出現不一致,那麼 cache coherence 核心在於保證多個核訪問同一數據時的一致。

volatile 關鍵字可以說是針對寄存器層面的,volatile 告訴編譯器要去內存中將數據重新載入一遍,而不要直接使用已經 load 到寄存器中的數據,這個工作層級比 cache 一致性協議高一層。

再抽象一點,如果是一個非底層程序員,只考慮程序正確性而不考慮類似 false sharing 以及 cache miss 帶來的性能損耗(僅僅是性能損耗,完全不影響正確性),你可以完全不用管 cache coherence 是怎麼實現的,也完全不用去了解。但是,如果你不了解 memory ordering 以及 volatile 如何使用,你的並發程序可能就會出錯?前者完全不需要你管,後者需要你自己的代碼來控制,這就是區別。

建議你先去了解 memory ordering 的涉及到的內容,我給你一些關鍵信息你可以去 google:

* acquire-release semantics

* Sequential consistency

* Synchronize-with relation

* Strongly- or Weakly-ordered memory

搞清楚了 memory ordering 之後你再去了解 cache coherence,兩者之間的聯繫才能搞清楚。

順便說一句,不要指望知乎上面問個問題就能把這一套弄清楚,自己去看資料吧


緩存是對程序透明的,程序就知道寫內存讀內存。緩存一致性協議解決的問題是,一個寫或者讀操作的結果,是跟真正訪問內存的結果是一樣的。每個CPU讀這個地址都能讀出一樣的結果,一個CPU寫了以後,其他CPU再讀是讀到更新了的結果。據我所知intel用的是類似MESIF的協議。這個是晶元邏輯層面來保證。

atomic操作,內存柵欄是保證各個線程能夠按一定的順序去訪問內存。這個線程再修改這個內存地址的時候,另一個不要來摻合。否則結果會不可預料。這個是軟體層面的保證。


1. 普通變數能保證的是最終一致性,但是並不代表立刻就一致。所以答案應該算是:否

2. voilate是說每次都從內存去讀,和每次都寫到內存,和一致性沒什麼關係。atomic操作更重要的還是在CPU的管線上,不在內存上。而且還涉及影響編譯優化的時候執行順序優化。

3. 涉及。

4. CPU層面我不是很了解,不過映像中是:都有


推薦閱讀:

為什麼我國的計算機科技領域發展了十幾年水平依舊落後國外這麼多?
如何看待 AMD Ryzen 使用的神經網路預測和智能預取特性?
計算機為什麼要設置線性地址,從邏輯地址到線性地址再到物理地址?
如何看待AMD Ryzen 1700實測GTA V遊戲性能不如Intel Core i7 7700k?
如果CPU的cache(緩存)容量上GB或更高,會有哪些不同?

TAG:CC | 計算機體系架構 | 中央處理器CPU計算機體系架構 |