Why Memory Barriers?中文翻譯(上)


Why Memory Barriers?中文翻譯(上)

本文是對perfbook的附錄C Why Memory Barrier的翻譯,希望通過對大師原文的翻譯可以彌補之前譯者發布的關於memory barrier的一篇很拙劣的文章的遺憾。

本文的翻譯不是一一對應的翻譯,主要是領會精神,用自己的語言表述,最優先保證的是中文表述的流暢而不是和原文保持一致(希望可以做到)。由於水平有限,歡迎指正。

一、前言

到底是什么原因導致CPU的設計者把memory barrier這樣的大招強加給可憐的,不知情的SMP軟件設計者?

一言以蔽之,性能,因為對內存訪問順序的重排可以獲取更好的性能,如果某些場合下,程序的邏輯正確性需要內存訪問順序和program order一致,例如:同步原語,那么SMP軟件工程師可以使用memory barrier這樣的工具阻止CPU對內存訪問的優化。

如果你想了解更多,需要充分理解CPU cache是如何工作的以及如何讓CPU cache更好的工作,本文的主要內容包括:

1、 描述cache的結構

2、 描述cache-coherency protocol如何保證cache一致性

3、 描述store buffers和invalidate queues如何獲取更好的性能

在本文中,我們將了解到memory barrier為何是一個必須存在的雙刃劍,一方面,它對性能和擴展性有很大的殺傷力,另外一方面,為了程序的邏輯正確,memory barrier這個雙刃劍必須存在。而之所以會有memory barrier這個雙刃劍是由於CPU的速度要快於(數量級上的差異)memory以及他們之間的互連器件(interconnect)。

二、cache的結構

現代CPU的速度要遠快於memory system。一個2006年的CPU可以每ns執行10條指令,但是卻需要幾十個ns來從main memory中獲取數據。這個速度的差異(超過2個數量級)使得現代CPU一般會有幾個MB的cache。當然這些cache可以分成若干的level,最靠近CPU那個level的cache可以在一個cycle內完成memory的訪問。我們抽象現代計算機系統的cache結構如下:

mcscs

CPU cache和memory系統使用固定大小的數據塊來進行交互,這個數據塊被稱為cache line,cache line的size一般是2的整數次冪,根據設計的不同,從16B到256B不等。

cache miss:

當CPU首次訪問某個數據的時候,它沒有在cpu cache中,我們稱之為cache miss(更准確的說法是startup或者warmup cache miss)。

在這種情況下,cpu需要花費幾百個cycle去把該數據對應的cacheline從memory中加載到cpu cache中,而在這個過程中,cpu只能是等待那個耗時內存操作完成。

一旦完成了cpu cache數據的加載,隨后的訪問會由於數據在cache中而使得cpu全速運行。

capacity miss:

運行一段時間之后,cpu cache的所有cacheline都會被填充有效的數據,這時候的,要加載新的數據到cache中必須將其他原來有效的cache數據“強制驅離”(一般

選擇最近最少使用的那些cacheline)。這種cache miss被稱為capacity miss,因為CPU cache的容量有限,必須為新數據找到空閑的cacheline。

有的時候,即便是cache中還有idle的cacheline,舊的cache數據也會被“強制驅離”,以便為新的數據加載到cacheline中做准備。

當然,這是和cache的組織有關。size比較大的cache往往實現成hash table(為了硬件性能),所有的cache line被分成了若干個固定大小的hash buckets(更專

業的術語叫做set),這些hash buckets之間不是形成鏈表,而是類似陣列,具體如下圖所示:

cache set

圖中的cache一共有32個cacheline,被組織成16個set,每個set有2個可選的cacheline,分別稱之為way 0和way 1。每個cacheline有256個Byte,在cache和memory交互cacheline的時候,要求cache line中數據地址對齊在256個字節上。256B的cacheline size稍顯大了一點,主要是為了16進制的算法簡單一些,實際中,level 0的cpu cache一般沒有這么大。如果用專業術語來說的話,上面的這種cache被稱為two-way set-associative cache。

這種cache的組織類似軟件中的有16個buckets的hash table,每個buckets(注意,這里是復數)中有兩個bucket,最多可以放兩個數據元素。total cache size(32個cache line)以及associativity(2 way)被稱為cache的幾何結構。由於是硬件實現,因此hash function(選擇哪一個buckets)非常簡單:從memory address中選擇4個bit即可。

在上圖中,每一個cell表示一個cache line,保存256B數據,

問: 為什么每個cacheline可以保存256B數據??

答:這邊我一直沒有搞明白為什么可以保存256B數據 ,后面我明白了。 這是因為set0(0x0)way0 ,這個cacheline 可以存放的地址范圍是【0x123456000- 0x 123456EFF 】,每個地址存放一個 字節,那么一個cacheline就可以存放256個字節

空的cell表示該cacheline中沒有數據,是idle狀態的,緩存數據的cacheline標記了其保存數據對應的memory address。由於有256B對齊的要求,因此地址的低8位都是0,而8~11這四個bit用來選擇set。

當程序順序訪問了0x12345000 到0x12345EFF之間的數據的時候,cache中的前15個set的way 0 cacheline都被加載了數據。

隨后對0x43210E00 到0x43210EFF數據的訪問,導致cache的第15個set的way 1 cacheline也被加載的數據。

問: 為什么0x43210E00 到0x43210EFF 是在cache的第15個set的way 1 cacheline 中,

答: 這是因為hash函數 映射 ,,前面說了地址的 8~11這四個bit用來選擇set ,0x43210E00 是映射到 set 15(oxE)的,因為set 15(oxE) way0 已經被使用了,所以使用了0x43210E00 到0x43210EFF 使用了set 15(oxE) way0 .。

OK,上圖中的cache的狀態就是這樣的。這時候,我們一起看看后續cache的操作情況。

如果程序訪問0x1233000地址的數據,那么set 0(Oxo)被選中,由於way 0已經是保存了數據,因此way 1被用來緩存本次數據訪問的內容。

如果程序訪問0x12345F00地址的數據,那么set 15(OxF)被選中,由於way 0和way 1都是idle的,因此way 0被用來緩存本次數據訪問的內容。

associativity miss:

但是,如果訪問0x1233E00這個地址開始的256B數據塊的時候,問題來了,這時候,set 14(0xE)已經滿了,way 0和way 1這兩個cacheline都加載了數據,

怎么辦?當然是把其中之一趕出去,為新來的數據讓出地方。如果被趕出去的數據隨后又被訪問,這時候的cache miss被稱為associativity miss。

到目前為止,我們只考慮了CPU讀取數據的情況,如果寫入數據會怎樣呢?

在某個CPU寫入數據之前,有一點很重要,即所有的CPU需要對該數據的內容達成共識。

因此,A cpu寫入之前,需要先將其他cpu cache中的數據設定為無效。

只有這個操作完成之后,A cpu才能安全的寫入數據,而不會造成一致性的問題。

write miss:

如果該數據已經在A cpu的cache中,但是是read only的,這時候,該cpu不能直接操作cacheline中的對應的數據(因為是read only的),這種cache miss被叫做write miss。

一旦A cpu完成了invalidate其他cpu cache中的數據,該cpu可以不斷的寫或者讀取其cache中的數據。

(注意:為了表述方便,我這里給指定cpu命名為A)

communication miss:

稍后,如果其他cpu也要訪問該數據,由於其他CPU的cache數據已經被設置為無效,因此,其他cpu的訪問會導致cache miss。

之所以如此,是因為前面A CPU在寫入數據的時候,將其他CPU的cache數據設置為無效,這種cache miss被稱為communication miss。

之所以稱為communication miss,是因為這種cache miss的發生是由於多個CPU使用共享內存進行通信(例如:互斥算法中的lock)。

三、cachecoherency protocols

毫無疑問,系統中的各個CPU在進行數據訪問的時候有自己的視角(通過自己的cpu cache),因此小心的維持數據的一致性變得非常重要。如果不仔細的進行設

計,有可能在各個cpu這對自己特定的CPU cache進行加載cacheline、設置cacheline無效、將數據寫入cacheline等動作中,把事情搞糟糕,例如數據丟失,或者

更糟糕一些,不同的cpu在各自cache中看到不同的值。這些問題可以通過cachecoherency protocols來保證,也就是下一節的內容。

Cache-coherency協議用來管理cacheline的狀態,從而避免數據丟失或者數據一致性問題。這些協議可能非常復雜,定義幾十個狀態,本節我們只關心MESI

cache-coherence 協議中的四個狀態。

1、MESI狀態

MESI是“modified”, “exclusive”, “shared”, 和 “invalid”首字母的大寫,當使用MESI cache-coherence 協議的時候,cacheline可以處於這四個狀態中的一個,因此,HW工程師設計cache的時候,除了物理地址和具體的數據之外,還需要為每一個cacheline設計一個2-bit的tag來標識該cacheline的狀態。

modified:

處於modified狀態的cacheline說明近期有過來自對應cpu的寫操作,同時也說明該該數據不會存在其他cpu對應的cache中。因此,處於modified狀態的cacheline也可以說是被該CPU獨占。而又因為只有該CPU的cache保存了最新的數據(最終的memory中都沒有更新),所以,該cache需要對該數據負責到底。例如根據請求,該cache將數據及其控制權傳遞到其他cache中,或者cache需要負責將數據寫回到memory中,而這些操作都需要在reuse該cache line之前完成。

exclusive:

exclusive狀態和modified狀態非常類似,唯一的區別是對應CPU還沒有修改cacheline中的數據,也正因為還沒有修改數據,因此memory中對應的data也是最新的。在exclusive狀態下,cpu也可以不通知其他CPU cache而直接對cacheline進行操作,因此,exclusive狀態也可以被認為是被該CPU獨占。由於memory中的數據和cacheline中的數據都是最新的,因此,cpu不需對exclusive狀態的cacheline執行寫回的操作或者將數據以及歸屬權轉交其他cpu cache,而直接reuse該cacheline(將cacheine中的數據丟棄,用作他用)。

share:

處於share狀態的cacheline,其數據可能在一個或者多個CPU cache中,因此,處於這種狀態的cache line,CPU不能直接修改cacheline的數據,而是需要首先和其他CPU cache進行溝通。和exclusive狀態類似,處於share狀態的cacheline對應的memory中的數據也是最新的,因此,cpu也可以直接丟棄cacheline中的數據而不必將其轉交給其他CPU cache或者寫回到memory中。

invalid:

處於invalid狀態的cacheline是空的,沒有數據。當新的數據要進入cache的時候,優選狀態是invalid的cacheline,之所以如此是因為如果選中其他狀態的cacheline,則說明需要替換cacheline數據,而未來如果再次訪問這個被替換掉的cacheline數據的時候將遇到開銷非常大的cache miss。

由於所有的CPU需要通過其cache看到一致性的數據,因此cache-coherence協議被用來協調cacheline數據在系統中的移動。

2、MESI Protocol Messages

在上節中描述的各種狀態的遷移需要CPU之間的通信,如果所有CPU都是在一個共享的總線上的時候,下面的message就足夠了:

1、Read:

read message 用來獲取指定物理地址上的cacheline數據。

2、Read Response:

該消息攜帶了read message所請求的數據。

read response可能來自memory,也可能來自其他的cache。

例如:如果一個cache有read message請求的數據並且該cacheline的狀態是modified,那么該cache必須以read response回應這個read message,因為該

cache中保存了最新的數據。(前面說過read message是用來獲取指定物理地址上的cacheline數據)

這邊就是cpu0 的L1 D cache 就有來自cpu1 的read message請求.該請求需要獲取變量a的chacheline,

cpu0中帶有a 數據的cacheline當前所處於的狀態是 modify狀態的....... cpuo0 Read reponse cpu1

image-20211101215046566

Read 於 Read reponse 是一對存在的

3、Invalidate:

該命令用來將其他cpu cache中的數據設定為無效。該命令攜帶物理地址的參數,其他CPU cache在收到該命令后,必須進行匹配,發現自己的cacheline中有該物

理地址的數據,那么就將其移除並用Invalidate Acknowledge回應。

4、Invalidate Acknowledge:

收到invalidate message的cpu cache,在移除了其cache line中的特定數據之后,必須發送invalidate acknowledge消息。

invalidate 與 Invalidate Acknowledge是一對存在的

5、Read Invalidate: 重要打個五角星

read invalidate是一個常用的message ..

該message中也包括了物理地址這個參數,以便說明其想要讀取哪一個cacheline數據。

此外,該message還同時有invalidate message的功效,即其他的cache在收到該命令后,移除自己cacheline中的數據。

因此,Read Invalidate message實際上就是read + invalidate。發送Read Invalidate之后,cache期望收到一個read response以及多個invalidate

acknowledge。

6、Writeback:

Writeback。該message包括兩個參數,一個是地址,另外一個是寫回的數據。

該消息用在modified狀態的cacheline被驅逐出境(給其他數據騰出地方)的時候發出,該命名用來將最新的數據寫回到memory(或者其他的CPU cache中)。

**總結: **

有意思的是基於共享內存的多核系統其底層是基於消息傳遞的計算機系統。這也就意味着由多個SMP 機器組成的共享內存的cluster系統在兩個不同的level上使用

了消息傳遞機制,一個是SMP內部的message passing,另外一個是SMP機器之間的。

3、MESI State Diagram

根據protocol message的發送和接收情況,cacheline會在“modified”, “exclusive”, “shared”, 和 “invalid”這四個狀態之間遷移,具體如下圖所示:

mesi state

The transition arcs in this figure are as follows:

對上圖中的狀態遷移解釋如下:

1: M->E (即Transition (a): )

cache可以通過writeback transaction將一個cacheline的數據寫回到memory中(或者下一級cache中),這時候,該cacheline的狀態從Modified遷移到

Exclusive狀態。

對於cpu而言,cacheline中的數據仍然是最新的,而且是該cpu獨占的,因此可以不通知其他cpu cache而直接修改之。

image-20211101085819522

2:E->M ( 即Transition (b)😃

1、在Exclusive狀態下,cpu可以直接將數據寫入cacheline,不需要其他操作。

2、相應的,該cacheline狀態從Exclusive狀態遷移到Modified狀態。這個狀態遷移過程不涉及bus上的Transaction(即無需MESI Protocol Messages的交互)。

3: M->I (即Transition (c)😃 重要 重要

The CPU receives a “read invalidate”message for a cache line that it has modified.

The CPU must invalidate its local copy, then respond with both a “read response” and an “invalidate acknowledge” message, both sending the data to the

requesting CPU and indicating that it no longer has a local copy.

CPU 在總線上收到一個read invalidate的請求,同時,該請求是針對一個處於modified狀態的cacheline,

在這種情況下,CPU必須將cacheline狀態設置為無效並且用read response”和“invalidate acknowledge來回應收到的read invalidate的請求,完成整個

bus transaction一旦完成這個transaction,數據被送往其他cpu cache中,本地的copy已經不存在了。

問題1:為啥感覺這邊的cachline狀態涉及到了兩個不同cpu的呢? 首先M狀態應該是 發送read invalid 消息的cpu 上的cacheline , 而I 狀態應該是接受read invaild 的cpu 上的cacheline... 還是我理解的有問題???

4:I->M(即Transition (d): ) 重要 重要

The CPU does an atomic readmodify-write operation on a data item that was not present in its cache. It transmits a “read invalidate”, receiving the data via a “read response”. The CPU can complete the transition once it has also received a full set of “invalidate acknowledge” responses.

CPU需要執行一個原子的readmodify-write操作,並且其cache中沒有緩存數據,這時候,CPU就會在總線上發送一個read invalidate用來請求數據,同時想獨自霸占對該數據的所有權。該CPU的cache可以通過read response獲取數據並加載cacheline,同時,為了確保其獨占的權利,必須收集所有其他cpu發來的invalidate acknowledge之后(其他cpu沒有local copy),完成1整個bus transaction。

5: S->M(即Transition e)

The CPU does an atomic readmodify-write operation on a data item that was previously read-only in its cache. It must transmit “invalidate” messages, and must wait for a full set of “invalidate acknowledge” responses before completing the transition.

CPU需要執行一個原子readmodify-write操作,並且其local cache有read only的緩存數據(cacheline處於shared狀態),這時候,CPU就會在總線上發送

一個invalidate請求其他cpu清空自己的local copy以便完成其獨自霸占對該數據的所有權的夢想。**同樣的,該cpu必須收集所有其他cpu發來的invalidate **

acknowledge之后,才算完成整個bus transaction

問題2:CPU需要執行一個原子readmodify-write操作,並且其local cache中有read only的緩存數據(cacheline處於shared狀態),, 這邊的 readmodify-write 操作跟read only 是否有矛盾呢???

6: M->S(即Transition (f))

Some other CPU reads the cache line, and it is supplied from this CPU’s cache, which retains
a read-only copy, possibly also writing it back to memory. This transition is initiated by the reception of a “read” message, and this CPU responds with a “read response” message containing the requested data.

在本cpu獨自享受獨占數據的時候,其他的cpu發起read請求,希望獲取數據,這時候,本cpu必須以其local cacheline的數據回應,並以read response回應之前

總線上的read請求。這時候,本cpu失去了獨占權,該cacheline狀態從Modified狀態變成shared狀態(有可能也會進行寫回的動作)。

7:E->S (即Transition (g): )

Some other CPU reads a data item in this cache line, and it is supplied either from this CPU’s cache or from memory. In either case, this CPU retains a read-only copy. This transition is initiated by the reception of a “read” message, and this CPU responds with a “read response” messagecontaining the requested data.

這個遷移和f類似,只不過開始cacheline的狀態是exclusive,cacheline和memory的數據都是最新的,不存在寫回的問題。總線上的操作也是在收到read請求之

后,以read response回應。

8:S->E(即Transition (h)😃

This CPU realizes that it will soon need to write to some data item in this cache line, and thus transmits an “invalidate” message. The CPU cannot complete the transition until it receives a full set of “invalidate acknowledge” responses. Alternatively, all other CPUs eject this cache line from their caches via “writeback” messages (presumably to make room for other cache lines), so that this CPU is the last CPU caching it.

方式一: 如果cpu認為自己很快就會啟動對處於shared狀態的cacheline進行write操作,因此想提前先霸占上該數據。

因此,該cpu會發送invalidate敦促其他cpu清空自己的local copy,當收到全部其他cpu的invalidate acknowledge之后,transaction完成,本cpu上對應的

cacheline從shared狀態切換exclusive狀態。

方式二: 還有另外一種方法也可以完成這個狀態切換:當所有其他的cpu對其local copy的cacheline進行寫回操作,同時將cacheline中的數據設為無效(主要是

為了為新的數據騰些地方),這時候,本cpu坐享其成,直接獲得了對該數據的獨占權。

9:E->I (Transition (i): )

Some other CPU does an atomic read-modify-write operation on a data item in a cache line held only in this CPU’s cache, so this CPU invalidates it from its cache. This transition is initiated by the reception of a “read invalidate” message, and this CPU responds with both a “read response” and an “invalidate acknowledge” message.

其他的CPU進行一個原子的read-modify-write操作,但是,數據在本cpu的cacheline中,因此,其他的那個CPU會發送read invalidate,請求對該數據以及獨占

權。本cpu回送read response”和“invalidate acknowledge”,一方面把數據轉移到其 他cpu的cache中,另外一方面,清空自己的cacheline。

10: I->E (Transition (j): ) 重要 重要

This CPU does a store to a data item in a cache line that was not in its cache, and thus transmits a “read invalidate” message. The CPU cannot complete the transition until it receives the “read response” and a full set of “invalidate acknowledge” messages. The cache line will presumably transition to “modified” state via transition (b) as soon as the actual store completes.

cpu想要進行write的操作但是數據不在local cache中,因此,該cpu首先發送了read invalidate啟動了一次總線transaction。在收到read response回應拿到數

據,並且收集所有其他cpu發來的invalidate acknowledge之后(確保其他cpu沒有local copy),完成整個bus transaction。

當write操作完成之后,該cacheline的狀態會從Exclusive狀態遷移到Modified狀態。

問題: 首先I->E 的狀態切換是I->M的中間狀態 ,查看[4:I->M(即Transition (d): ) 重要 重要] 里面列出來的。。。

11: I->S (Transition (k)😃

This CPU loads a data item in a cache line that was not in its cache. The CPU transmits a “read” message, and completes the transition upon receiving the corresponding “read response”.

本CPU執行讀操作,發現local cache沒有數據,因此通過read發起一次bus transaction,來自其他的cpu local cache或者memory會通過read response回應,從

而將該cacheline從Invalid狀態遷移到shared狀態。

12:S->I(Transition (l): )

Some other CPU does a store to a data item in this cache line, but holds this cache line in read-only state due to its being held in other CPUs’ caches (such as the current CPU’s cache). This transition is initiated by the reception of an “invalidate” message, and this CPU responds with an “invalidate acknowledge” message.

當cacheline處於shared狀態的時候,說明在多個cpu的local cache中存在副本,因此,這些cacheline中的數據都是read only的,一旦其中一個cpu想要執行數據

寫入的動作,必須先通過invalidate獲取該數據的獨占權,而其他的CPU會以invalidate acknowledge回應,清空數據並將其cacheline從shared狀態修改成invalid

狀態。

4、MESI Protocol Example

Let’s now look at this from the perspective of a cache line’s worth of data, initially residing in memory at address 0, as it travels through the various single-line direct-mapped caches in a four-CPU system. Table C.1 shows this flow of data, with the first column showing the sequence of operations, the second the CPU performing the operation, the third the operation being performed, the next four the state of each CPU’s cache line (memory address followed by MESI state), and the final two columns whether the corresponding memory contents are up to date (“V”) or not (“I”).

OK,在理解了各種cacheline狀態、各種MESI協議消息以及狀態遷移的描述之后,我們從cache line數據的角度來看看MESI協議是如何運作的。開始,數據保存在memory的0地址中,隨后,該數據會穿行在四個CPU的local cache中。為了方便起見,我們讓CPU local cache使用最簡單的Direct-mapped的組織形式。具體的過程可以參考下面的圖片:

cache ex

第一列是操作序列號, 第二列是執行操作的CPU,第三列是具體執行哪一種操作,第四列描述了各個cpu local cache中的cacheline的狀態(用meory address/狀態表示),最后一列描述了內存在0地址和8地址的數據內容的狀態:V表示是最新的,和cache一致,I表示不是最新的內容,最新的內容保存在cache中。

Initially, the CPU cache lines in which the data would reside are in the “invalid” state, and the data is valid in memory. When CPU 0 loads the data at address 0, it enters the “shared” state in CPU 0’s cache, and is still
valid in memory. CPU 3 also loads the data at address 0, so that it is in the “shared” state in both CPUs’ caches, and is still valid in memory. Next CPU 0 loads some other cache line (at address 8), which forces the data at
address 0 out of its cache via an invalidation, replacing it with the data at address 8. CPU 2 now does a load from address 0, but this CPU realizes that it will soon need to store to it, and so it uses a “read invalidate” message in order to gain an exclusive copy, invalidating it from CPU 3’s cache (though the copy in memory remains up to
date). Next CPU 2 does its anticipated store, changing the state to “modified”. The copy of the data in memory is
now out of date. CPU 1 does an atomic increment, using a “read invalidate” to snoop the data from CPU 2’s cache and invalidate it, so that the copy in CPU 1’s cache is in the “modified” state (and the copy in memory remains out of date). Finally, CPU 1 reads the cache line at address 8, which uses a “writeback” message to push address 0’s data back out to memory.

sequence 0:

最開始的時候 ,各個cpu cache中的cacheline都是Invalid狀態,而Memory中的數據都保存了最新的數據。

sequence 1:

隨后 CPU 0執行了load操作,將address 0的數據加載到寄存器,這個操作使得 【保存0地址數據的那個cacheline】從invalid狀態遷移到shared狀態。

 問題3:這邊我咋覺得是是 invaild --》 exclusive???????

sequence 2:

隨后(sequence 2),CPU3也對0地址執行了load操作,導致其local cache上對應的cacheline也切換到shared狀態。當然,這時候,memory仍然是最新的。

sequence 3:

在sequence 3中,CPU 0執行了對地址8的load操作,由於地址0和地址8都是選擇同一個cache set,而且,我們之前已經說過,該cache是direct-mapped的(即每個set只有一個cacheline),因此需要首先清空該cacheline中的數據(該操作被稱為Invalidation),由於cacheline的狀態是shared,因此,不需要通知其他CPU。Invalidation local cache上的cacheline之后,cpu 0的load操作將該cacheline狀態修改成Shared狀態(保存地址8的數據)。

問題4;為啥我總感覺 cpu0 中的 是 8/E,CPU3 中的是0/E?????

cache ex

sequence 4:

CPU 2也開始執行load操作了(sequence 4),雖然是load操作,但是CPU知道程序隨后會修改該值(不是原子操作的read-modify-write,否就是遷移到Modified狀態了,也不是單純的load操作,否則會遷移到shared狀態),因此向總線發送了read invalidate命令,一方面獲取該數據(自己的local cache中沒有地址0的數據),另外,CPU 2想獨占該數據(因為隨后要write)。這個操作導致CPU 3的cacheline遷移到invalid狀態。當然,這時候,memory仍然是最新的有效數據。

問題5:(不是原子操作的read-modify-write,否就是遷移到Modified狀態了,也不是單純的load操作,否則會遷移到shared狀態)這個句子想要表達什么意思

Sequence 5:

CPU 2的store操作很快到來(Sequence 5),由於准備工作做的比較充分(Exclusive狀態,獨占該數據),cpu直接修改cacheline中的數據(對應地址0),從而將其狀態遷移到modified狀態,同時要注意的是:memory中的數據已經失效,不是最新的數據了,任何其他CPU發起對地址0的load操作都不能從memory中讀取,而是通過嗅探(snoop)的方式從CPU 2的local cache中獲取。

sequence 6:

在sequence 6中,CPU 1對地址0的數據執行原子的加1操作,這時候CPU 1會發出read invalidate命令,將地址0的數據從CPU 2的cacheline中嗅探得到,同時通過invalidate其他CPU local cache的內容而獲得獨占性的數據訪問權。

這時候,CPU 2中的cacheline狀態變成invalid狀態,而CPU 1將從invalid狀態遷移到modified狀態。

sequence 7:

最后(sequence 7),CPU 1對地址8進行load操作,由於cacheline被地址0占據,因此需要首先將其驅逐出cache,於是執行write back操作將地址0的數據寫回到memory,同時發送read命名令,從CPU 0的cache中獲得數據加載其cacheline,最后,CPU1的cache變成shared狀態(保存地址8的數據)。由於執行了write back操作,memory中地址0的數據又變成最新的有效數據了。

四、Stores Result in Unnecessary Stalls

Although the cache structure shown in Figure C.1 provides good performance for repeated reads and writes from a given CPU to a given item of data, its performance for the first write to a given cache line is quite poor. To see this, consider Figure C.4, which shows a timeline of a write by CPU 0 to a cacheline held in CPU 1’s cache. Since CPU 0 must wait for the cache line to arrive before it can write to it, CPU 0 must stall for an extended period of time.

在上面的現代計算機cache結構圖,我們可以看出,針對某些特定地址的數據(在一個cacheline中)重復的進行讀寫,這種結構可以獲得很好的性能,不過,對於第一次寫,其性能非常差。下面的這個圖可以展示為何寫性能差:

stall

cpu 0發起一次對某個地址的寫操作,但是local cache沒有數據,該數據在CPU 1的local cache中,因此,為了完成寫操作,CPU 0發出 invalidate的命令,

invalidate其他CPU的cache數據,cpu1 invalidate自己的local cache之后,並且發送了 invaliate ack = 0k ,只有完成了這些總線上的transaction之后, CPU 0才能正

在發起寫的操作,這是一個漫長的等待過程。

But there is no real reason to force CPU 0 to stall for so long — after all, regardless of what data happens to be in the cache line that CPU 1 sends it, CPU 0 is going to unconditionally overwrite it.

但是,其實沒必要等待這么長的時間,畢竟,物理CPU 1中的cacheline保存有什么樣子的數據,其實都沒有意義,這個值都會被CPU 0新寫入的值覆蓋的。

1、Store Buffers

One way to prevent this unnecessary stalling of writes is to add “store buffers” between each CPU and its cache,
as shown in Figure C.5. With the addition of these store buffers, CPU 0 can simply record its write in its store buffer and continue executing. When the cache line does finally make its way from CPU 1 to CPU 0, the data will be moved from the store buffer to the cache line.

有一種可以阻止cpu進入無聊等待狀態的方法就是在CPU和cache之間增加store buffer這個HW block,如下圖所示:

store buffer

一旦增加了store buffer,那么cpu0無需等待其他CPU的相應,只需要將要修改的內容放入store buffer,然后繼續執行就OK了。

當cacheline完成了bus transaction,並更新了cacheline的狀態后,要修改的數據將從store buffer進入cacheline。

These store buffers are local to a given CPU or, on systems with hardware multithreading, local to a given core. Either way, a given CPU is permitted to access only the store buffer assigned to it. For example, in Figure C.5, CPU 0 cannot access CPU 1’s store buffer and vice versa. This restriction simplifies the hardware by separating concerns: The store buffer improves performance for consecutive writes, while the responsibility for communicating among CPUs (or cores, as the case may be) is fully shouldered by the cache-coherence protocol. However, even given this restriction, there are complications that must be addressed, which are covered in the next two sections.

這些store buffer對於cpu而言是local的,如果系統是硬件多線程, 那么每一個cpu core擁有自己私有的stroe buffer,一個cpu只能訪問自己私有的那個store buffer。

在上圖中,cpu 0不能訪問cpu1的store buffer,反之亦然。之所以做這樣的限制是為了模塊划分(各個cpu core模塊關心自己的事情,讓cache系統維護自己的操作),讓硬件設計變得簡單一些。

store buffer增加了CPU連續寫的性能,同時把各個CPU之間的通信的任務交給維護cache一致性的協議。即便給每個CPU分配私有的store buffer,仍然引入了一

些復雜性,我們會在下面兩個小節中描述。

2、Store Forwarding

To see the first complication, a violation of selfconsistency, consider the following code with variables “a” and “b” both initially zero, and with the cache line containing variable “a” initially owned by CPU 1 and that containing “b” initially owned by CPU 0:

上文提到store buffer引入了復雜性,

我們先看第一個例子:本地數據不一致的問題。我們先看看下面的代碼:

1 a = 1;
2 b = a + 1;
3 assert(b == 2);

a和b都是初始化為0,並且變量a在CPU 1的cacheline中,變量b在CPU 0的cacheline中。

image-20211031190748599

One would not expect the assertion to fail. However, if one were foolish enough to use the very simple architecture
shown in Figure C.5, one would be surprised. Such a system could potentially see the following sequence of events:

如果cpu執行上述代碼,那么第三行的assert不應該失敗,不過,如果CPU設計者使用上圖中的那個非常簡單的store buffer結構,那么你應該會遇到“驚喜”(assert失敗了)。具體的執行過程是這樣的:

(1) CPU 0執行a=1的賦值操作

(2) CPU 0 looks “a” up in the cache, and finds that it is missing. CPU 0遇到cache miss

(3) CPU 0 therefore sends a “read invalidate” message in order to get exclusive ownership of the cache line containing “a”.

CPU 0發送read invalidate消息以便從CPU 1那里獲得數據,並invalid其他cpu保存a數據的local cacheline。

這邊 cpu0 中關於a 變量 的cache line 狀態就是 從 Invaile =》 modifyed 狀態的

image-20211101224634808

(4)CPU 0 records the store to “a” in its store buffer. CPU 0把要寫入的數據“1”放入store buffer

(5)CPU 1 receives the “read invalidate” message, and responds by transmitting the cache line and removing that cacheline from its cache. CPU 1收到read invalidate后回應,把本地cacheline的數據發送給CPU 0並清空本地cache中a的數據

問題6:不知道是不是下面的 狀態轉換

這邊 關於a 變量 的cache line 狀態就是 從 modifyed=》 invaild狀態的??????????????????不知道是不是下面的 我亂寫的

image-20211031192658394

(6) CPU 0 starts executing the b = a + 1. CPU 0執行b = a + 1

(7)CPU 0 receives the cache line from CPU 1, which still has a value of zero for “a”. CPU 0 收到來自CPU 1的數據,該數據是“0”

(8)CPU 0 loads “a” from its cache, finding the value zero. CPU 0從cacheline中加載a,獲得0值

(9)CPU 0 applies the entry from its store buffer to the newly arrived cache line, setting the value of “a” in its cache to one. CPU 0將store buffer中a的值寫入cacheline,這時候cache中的a值是“1”

(10)CPU 0 adds one to the value zero loaded for “a”above, and stores it into the cache line containing “b”(which we will assume is already owned by CPU 0). a = 0 已經被加載到Cpu0的寄存器中CPU 0執行a+1,得到1並將該值寫入b

(11)CPU 0 executes assert(b == 2), which fails. OMG,你期望b等於2,但是實際上b等於了1

image-20211031200030656

The problem is that we have two copies of “a”, one in the cache and the other in the store buffer.

導致這個問題的根本原因是我們有兩個a值,一個在cacheline中,一個在store buffer中。

This example breaks a very important guarantee, namely that each CPU will always see its own operations
as if they happened in program order. Breaking this guarantee is violently counter-intuitive to software types, so much so that the hardware guys took pity and implemented “store forwarding”, where each CPU refers to (or “snoops”) its store buffer as well as its cache when performing loads, as shown in Figure C.6. In other words, a given CPU’s stores are directly forwarded to its subsequent loads, without having to pass through the cache.

上面這個出錯的例子之所以發生是因為它違背了一個基本的原則,即每個CPU按照其視角來觀察自己的行為的時候必須是符合program order的。一旦違背這個原則,會導致一些非常不直觀的軟件行為,對軟件工程師而言就是災難。還好,有”好心“的硬件工程師幫助我們,修改了CPU的設計如下:

![store buffer forward](C:\Users\tangli\Downloads\並發編程基礎 (1)\d50fa77d45ffd0d7e634799c7c74269a20151210111059.gif)

這種設計叫做store forwarding,當CPU執行load操作的時候,不但要看cache,還有看store buffer是否有內容,如果store buffer有該數據,那么就采用store buffer中的值。

因此,即便是store操作還沒有寫入cacheline,store forwarding的效果看起來就好象cpu的store操作被向前傳遞了一樣(后面的load的指令可以感知到這個store操作) 。

With store forwarding in place, item 8 in the above sequence would have found the correct value of 1 for “a”in the store buffer, so that the final value of “b” would have been 2, as one would hope.

有了store forwarding的設計,上面的步驟(8)中就可以在store buffer獲取正確的a值是”1“而不是”0“,因此計算得到的b的結果就是2,和我們預期的一致了。

3、Store Buffers and Memory Barriers

To see the second complication, a violation of global memory ordering, consider the following code sequences with variables “a” and “b” initially zero:

關於store buffer引入的復雜性,我們再來看看第二個例子:

1 void foo(void)
2 {
3 a = 1;
4 b = 1;
5 }
6
7 void bar(void)
8 {
9 while (b == 0) continue;
10 assert(a == 1);
11 }

同樣的,a和b都是初始化成0.

Suppose CPU 0 executes foo() and CPU 1 executes bar(). Suppose further that the cache line containing “a” resides only in CPU 1’s cache, and that the cache line containing “b” is owned by CPU 0. Then the sequence of operations might be as follows:

我們假設CPU 0執行foo函數,CPU 1執行bar函數。我們再進一步假設a變量在CPU 1的cache中,b在CPU 0 cache中,執行的操作序列如下:

(1)CPU 0 executes a = 1. The cache line is not in CPU 0’s cache, so CPU 0 places the new value of “a” in its store buffer and transmits a “read invalidate”message.

CPU 0執行a=1的賦值操作,由於a不在local cache中,因此,CPU 0將a值放到store buffer中之后,發送了read invalidate命令到總線上去。

(2) CPU 1 executes while (b == 0) continue, but the cache line containing “b” is not in its cache. It therefore transmits a “read” message.

CPU 1執行 while (b == 0) 循環,由於b不在CPU 1的cache中,因此,CPU1發送一個read message到總線上,看看是否可以從其他cpu的local cache中或者memory中獲取數據

(3) CPU 0 executes b = 1. It already owns this cache line (in other words, the cache line is already in either the “modified” or the “exclusive” state), so it stores the new value of “b” in its cache line.

CPU 0繼續執行b=1的賦值語句,由於b就在自己的local cache中(cacheline處於modified狀態或者exclusive狀態),因此CPU0可以直接操作將新的值1寫入cache line

**(4) CPU 0 receives the “read” message, and transmits the cache line containing the now-updated value of “b” to CPU 1, also marking the line as “shared” in its own cache. **

CPU 0收到了read message,將最新的b值”1“回送給CPU 1,同時將b cacheline的狀態設定為shared

(5) CPU 1 receives the cache line containing “b” and installs it in its cache.

CPU 1收到了來自CPU 0的read response消息,將b變量的最新值”1“值寫入自己的cacheline,狀態修改為shared。

(6) CPU 1 can now finish executing while (b == 0) continue, and since it finds that the value of “b” is 1, it proceeds to the next statement. 由於b值等於1了,因此CPU 1跳出while (b == 0)的循環,繼續前行。

(7) CPU 1 executes the assert(a == 1), and, since CPU 1 is working with the old value of “a” , this assertion fails.

CPU 1執行assert(a == 1),這時候CPU 1的local cache中還是舊的a值,因此assert(a == 1)失敗。

(8) CPU 1 receives the “read invalidate” message, and transmits the cache line containing “a” to CPU 0 and invalidates this cache line from its own cache. But it is too late.

CPU 1收到了來自CPU 0的read invalidate消息,以a變量的值進行回應,同時清空自己的cacheline,但是這已經太晚了。

(9) CPU 0 receives the cache line containing “a” and applies the buffered store just in time to fall victim to CPU 1’s failed assertion.

CPU 0收到了read response和invalidate ack的消息之后,將store buffer中的a的最新值”1“數據寫入cacheline,然並卵,CPU 1已經assertion fail了。

增加內存屏障后解決上面的問題:

The hardware designers cannot help directly here, since the CPUs have no idea which variables are related, let alone how they might be related. Therefore, the hardware designers provide memory-barrier instructions to allow the software to tell the CPU about such relations. The program fragment must be updated to contain the memory barrier:

遇到這樣的問題,CPU設計者也不能直接幫什么忙,畢竟CPU並不知道哪些變量有相關性,這些變量是如何相關的。不過CPU設計者可以間接提供一些工具讓軟件工程師來控制這些相關性。這些工具就是memory-barrier指令。要想程序正常運行,必須增加一些memory barrier的操作,具體如下:

1 void foo(void)
2 {
3 a = 1;
4 smp_mb();
5 b = 1;
6 }
7
8 void bar(void)
9 {
10 while (b == 0) continue;
11 assert(a == 1);
12 }

個人筆記: 我們需要保證的就是: 首先a=1 在cpu1 上, cpu 0 對a=1 的賦值有兩步操作:

  1. 將a=1 的變量值放入刀 store buffer 緩存中

  2. 發送read invaliate message到cpu1 core中,清空 cpu1 core中的cacheline

  3. cpu1 獨占a cacheline

  4. 將storeBuffer的數據寫入到a的cacheline中

  5. cpu1讀取a的變量值需要發送read message到cpu o core中

smp_mb() 這個內存屏障的操作會在執行后續的store操作之前,首先flush store buffer(也就是將之前的值寫入到cacheline中)

smp_mb() 操作主要是為了讓數據在local cache中的操作順序是符合program order的順序的,為了達到這個目標有兩種方法:

方法一就是讓CPU stall,直到完成了清空了store buffer(也就是把store buffer中的數據寫入cacheline了)。

方法二是讓CPU可以繼續運行,不過需要在store buffer中做些文章,也就是要記錄store buffer中數據的順序,在將store buffer的數據更新到cacheline的操作

中,嚴格按照順序執行,即便是后來的store buffer數據對應的cacheline已經ready,也不能執行操作,

要等前面的store buffer值寫到cacheline之后才操作。 store buffe 寫道cacheline中的順序要有先來后到,誰先到誰先寫

增加smp_mb() 之后,操作順序如下:

(1)CPU 0執行a=1的賦值操作,由於a不在local cache中,因此,CPU 0將a值放到store buffer中之后,發送了read invalidate命令到總線上去。

(2) CPU 1執行 while (b == 0) 循環,由於b不在CPU 1的cache中,因此,CPU1發送一個read message到總線上,看看是否可以從其他cpu的local cache中或者memory中獲取數據

(3)CPU 0執行smp_mb()函數,給目前store buffer中的所有項做一個標記(后面我們稱之marked entries)。當然,針對我們這個例子,store buffer中只有一個marked entry就是“a=1”。

(4) CPU 0繼續執行b=1的賦值語句,雖然b就在自己的local cache中(cacheline處於modified狀態或者exclusive狀態),不過在store buffer中有marked entry,因此CPU0並沒有直接操作將新的值1寫入cache line,取而代之是b的新值”1“被寫入store buffer,當然是unmarked狀態。

(5)CPU 0收到了read message,將b值”0“(新值”1“還在store buffer中)回送給CPU 1,同時將b cacheline的狀態設定為shared。

(6) CPU 1收到了來自CPU 0的read response消息,將b變量的值(”0“)寫入自己的cacheline,狀態修改為shared。

(7)完成了bus transaction之后,CPU 1可以load b到寄存器中了(local cacheline中已經有b值了),當然,這時候b仍然等於0,因此循環不斷的loop。雖然b值在CPU 0上已經賦值等於1,但是那個新值被安全的隱藏在CPU 0的store buffer中。

(8)CPU 1收到了來自CPU 0的read invalidate消息,以a變量的值進行回應,同時清空自己的cacheline。

(9)CPU 0將store buffer中的a值寫入cacheline,並且將cacheline狀態修改為modified狀態。

(10)由於store buffer只有一項marked entry(對應a=1),因此,完成step 9之后,store buffer的b也可以進入cacheline了。不過需要注意的是,當前b對應的cacheline的狀態是shared

(11)CPU 0發送invalidate消息,請求b數據的獨占權

(12)CPU 1收到invalidate消息,清空自己的b cacheline,並回送acknowledgement給CPU 0。

(13) CPU 1繼續執行while (b == 0),由於b不在自己的local cache中,因此 CPU 1發送read消息,請求獲取b的數據。

(14)CPU 0收到acknowledgement消息,將b對應的cacheline修改成exclusive狀態,這時候,CPU 0終於可以將b的新值1寫入cacheline。

(15) CPU 0收到read消息,將b的新值1回送給CPU 1,同時將其local cache中b對應的cacheline狀態修改為shared。

(16) CPU 1獲取來自CPU 0的b的新值,將其放入cacheline中

(17) 由於b值等於1了,因此CPU 1跳出while (b == 0)的循環,繼續前行。

(18) CPU 1執行assert(a == 1),不過這時候a值沒有在自己的cacheline中,因此需要通過cache一致性協議從CPU 0那里獲得,這時候獲取的是a的最新值,也就是1值,因此assert成功。

通過上面的描述,我們可以看到,一個直觀上很簡單的給a變量賦值的操作,都需要那么長的執行過程,而且每一步都需要芯片參與,最終完成整個復雜的賦值操作過程。

五、Store Sequences Result in Unnecessary Stalls

不幸的是:每個cpu的store buffer不能實現的太大,其entry的數目不會太多。

當cpu以中等的頻率執行store操作的時候(假設所有的store操作導致了cache miss),store buffer會很快的被填滿。在這種狀況下,CPU只能又進入等待狀態,直到cache line完成invalidation和ack的交互之后,可以將store buffer的entry寫入cacheline,從而為新的store讓出空間之后,CPU才可以繼續執行。

這種狀況也可能發生在調用了memory barrier指令之后,因為一旦store buffer中的某個entry被標記了,那么隨后的store都必須等待invalidation完成,因此不管是否cache miss,這些store都必須進入store buffer。

引入invalidate queues可以緩解這個狀況。store buffer之所以很容易被填充滿,主要是其他CPU回應invalidate acknowledge比較慢,如果能夠加快這個過程,讓store buffer盡快進入cacheline,那么也就不會那么容易填滿了。

1、Invalidate Queues

invalidate acknowledge不能盡快回復的主要原因是invalidate cacheline的操作沒有那么快完成,特別是cache比較繁忙的時候,這時,CPU往往進行密集的loading和storing的操作,而來自其他CPU的,對本CPU local cacheline的操作需要和本CPU的密集的cache操作進行競爭,只要完成了invalidate操作之后,本CPU才會發生invalidate acknowledge。此外,如果短時間內收到大量的invalidate消息,CPU有可能跟不上處理,從而導致其他CPU不斷的等待。

然而,CPU其實不需要完成invalidate操作就可以回送acknowledgement消息,這樣,就不會阻止發生invalidate請求的那個CPU進入無聊的等待狀態。CPU可以buffer這些invalidate message(放入Invalidate Queues),然后直接回應acknowledgement,表示自己已經收到請求,隨后會慢慢處理。當然,再慢也要有一個度,例如對a變量cacheline的invalidate處理必須在該CPU發送任何關於a變量對應cacheline的操作到bus之前完成。

2、Invalidate Queues and Invalidate Acknowledge

有invalidate queue的系統結構如下圖所示:

invalidQ

有了Invalidate Queue的CPU,在收到invalidate消息的時候首先把它放入Invalidate Queue,同時立刻回送acknowledge 消息,無需等到該cacheline被真正invalidate之后再回應。

當然,如果本CPU想要針對某個cacheline向總線發送invalidate消息的時候,那么CPU必須首先去Invalidate Queue中看看是否有相關的cacheline,如果有,那么不能立刻發送,需要等到Invalidate Queue中的cacheline被處理完之后再發送。

一旦將一個invalidate(例如針對變量a的cacheline)消息放入CPU的Invalidate Queue,實際上該CPU就等於作出這樣的承諾:在處理完該invalidate消息之前,不會發送任何相關(即針對變量a的cacheline)的MESI協議消息。只要是對該cacheline的競爭不是那么劇烈,CPU還是對這樣的承諾很有信心的。

然而,緩存了invalidate消息也會引入一些其他的memory order的問題,我們在下一節討論。

3、Invalidate Queues and Memory Barriers

我們假設CPU緩存invalidation消息,在操作cacheline之前直接回應該invalidation消息。這樣的機制對於發送invalidation的CPU側是非常好的事,該CPU的store性能會非常高,但是會使內存屏障指令失效,我們來看看下面的例子:

1 void foo(void)
2 {
3 a = 1;
4 smp_mb();
5 b = 1;
6 }
7
8 void bar(void)
9 {
10 while (b == 0) continue;
11 assert(a == 1);
12 }

在上面的代碼片段中,我們假設a和b初值是0,並且a在CPU 0和CPU 1都有緩存的副本,即a變量對應的CPU0和CPU 1的cacheline都是shared狀態。

b處於exclusive或者modified狀態,被CPU 0獨占。我們假設CPU 0執行foo函數,CPU 1執行bar函數。

具體的操作序列如下:

(1) CPU 0執行a=1的賦值操作,由於a在CPU 0 local cache中的cacheline處於shared狀態,因此,CPU 0將a的新值“1”放入store buffer,並且發送了invalidate消息去清空CPU 1對應的cacheline。

(2) CPU 1執行while (b == 0)的循環操作,但是b沒有在local cache,因此發送read消息試圖獲取該值。

(3) CPU 1收到了CPU 0的invalidate消息,放入Invalidate Queue,並立刻回送Ack。

(4) CPU 0收到了CPU 1的invalidate ACK之后,即可以越過程序設定內存屏障(第四行代碼的smp_mb() ),這樣a的新值從store buffer進入cacheline,狀態變成Modified。

(5) CPU 0 越過memory barrier后繼續執行b=1的賦值操作,由於b值在CPU 0的local cache中,因此store操作完成並進入cache line。

(6) CPU 0收到了read消息后將b的最新值“1”回送給CPU 1,並修正該cacheline為shared狀態。

(7) CPU 1收到read response,將b的最新值“1”加載到local cacheline。

(8)對於CPU 1而言,b已經等於1了,因此跳出while (b == 0)的循環,繼續執行后續代碼

(9)但是由於這時候CPU 1 cache的a值仍然是舊值0,因此assertion 失敗

(10) 該來總會來,Invalidate Queue中針對a cacheline的invalidate消息最終會被CPU 1執行,將a設定為無效,但素,大錯已經釀成。

個人理解:

(我們不是應該保證cpu1 中對Invalidate Queue中針對a cacheline的invalidate消息 應該在 執行最后一行代碼 assert(a == 1)之前

完成的嗎??????? 保證a==1 的讀操作需要向 cpu0 發送read message ,從而獲取到最新a=1 的read reponse)

:[一旦將一個invalidate(例如針對變量a的cacheline)消息放入CPU的Invalidate Queue,實際上該CPU就等於作出這樣的承諾:在處理完該invalidate消息之前,不會發送任何相關(即針對變量a的cacheline)的MESI協議消息。只要是對該cacheline的競爭不是那么劇烈,CPU還是對這樣的承諾很有信心的。] 上面提到的這個沒有任何問題,但是a 也不應該讀取cpu1 本地local中的cacheline ,這個cacheline我們已經設置好要刪除了的啊 ...

很明顯,在上文中的場景中,加速Invalidation response導致foo函數中的memory barrier失效了,因此,這時候對Invalidation response已經沒有意義了,畢竟程序邏輯都錯了。

怎么辦?其實我們可以讓memory barrier指令和Invalidate Queue進行交互來保證確定的memory order。

具體做法是這樣的:當CPU執行memory barrier指令的時候,對當前Invalidate Queue中的所有的entry進行標注,這些被標注的項次被稱為marked entries,而隨后CPU執行的任何的load操作都需要等到Invalidate Queue中所有marked entries完成對cacheline的操作之后才能進行。因此,要想保證程序邏輯正確,我們需要給bar函數增加內存屏障的操作,具體如下:

1 void foo(void)
2 {
3 a = 1;
4 smp_mb();
5 b = 1;
6 }
7
8 void bar(void)
9 {
10 while (b == 0) continue;
11 smp_mb();
12 assert(a == 1);
13 }

With this change, the sequence of operations might be as follows:

程序修改之后,我們再來看看CPU的執行序列:

看懂了上面的例子,下面就很簡單了 ///

(1) CPU 0 executes a = 1. The corresponding cache line is read-only in CPU 0’s cache, so CPU 0 places
the new value of “a” in its store buffer and transmits an “invalidate” message in order to flush the corresponding cache line from CPU 1’s cache. CPU 0執行a=1的賦值操作,由於a在CPU 0 local cache中的cacheline處於shared狀態(read only),因此,CPU 0將a的新值“1”放入store buffer,並且發送了invalidate消息去清空CPU 1對應的cacheline。

(2) CPU 1 executes while (b == 0) continue, but the cache line containing “b” is not in its cache. It therefore transmits a “read” message. CPU 1執行while (b == 0)的循環操作,但是b沒有在local cache,因此發送read消息試圖獲取該值。

(3) CPU 1 receives CPU 0’s “invalidate” message, queues it, and immediately responds to it. CPU 1收到了CPU 0的invalidate消息,放入Invalidate Queue,並立刻回送Ack。

(4) CPU 0 receives the response from CPU 1, and is therefore free to proceed past the smp_mb() on line 4 above, moving the value of “a” from its store buffer to its cache line. CPU 0收到了CPU 1的invalidate ACK之后,即可以越過程序設定內存屏障(第四行代碼的smp_mb() ),這樣a的新值從store buffer進入cacheline,狀態變成Modified。

(5) CPU 0 executes b = 1. It already owns this cache line (in other words, the cache line is already in either the “modified” or the “exclusive” state), so it stores the new value of “b” in its cache line. CPU 0 越過memory barrier后繼續執行b=1的賦值操作(這時候該cacheline或者處於modified狀態,或者處於exclusive狀態),由於b值在CPU 0的local cache中,因此store操作完成並進入cache line。

(6) CPU 0 receives the “read” message, and transmits the cache line containing the now-updated value of “b” to CPU 1, also marking the line as “shared” in its own cache. CPU 0收到了read消息后將b的最新值“1”回送給CPU 1,並修正該cacheline為shared狀態。

(7) CPU 1 receives the cache line containing “b” and installs it in its cache. CPU 1收到read response,將b的最新值“1”加載到local cacheline。

(8) CPU 1 can now finish executing while (b == 0) continue, and since it finds that the value of “b” is 1, it proceeds to the next statement, which is now a memory barrier. 對於CPU 1而言,b已經等於1了,因此跳出while (b == 0)的循環,繼續執行memory barrier的代碼

(9) CPU 1 must now stall until it processes all preexisting messages in its invalidation queue. CPU 1現在不能繼續執行代碼,只能等待,直到Invalidate Queue中的message被處理完成

(10) CPU 1 now processes the queued “invalidate” message, and invalidates the cache line containing “a” from its own cache. CPU 1處理隊列中緩存的Invalidate消息,將a對應的cacheline設置為無效。

(11) CPU 1 executes the assert(a == 1), and, since the cache line containing “a” is no longer in CPU 1’s cache, it transmits a “read” message. 由於a變量在local cache中無效,因此CPU 1在執行assert(a == 1)的時候需要發送一個read消息去獲取a值。

(12) CPU 0 responds to this “read” message with the cache line containing the new value of “a”. CPU 0用a的新值1回應來自CPU 1的請求。

(13) CPU 1 receives this cache line, which contains a value of 1 for “a”, so that the assertion does not trigger. CPU 1獲得了a的新值,並放入cacheline,這時候assert(a == 1)不會失敗了。

With much passing of MESI messages, the CPUs arrive at the correct answer. This section illustrates why CPU designers must be extremely careful with their cachecoherence optimizations.

雖然多了很多MESI協議的交互,但是最終CPU的執行符合了預期的結果。這一節也說明了為什么CPU designer一定會非常小心的處理cache一致性的問題。

六、Read and Write Memory Barriers

在我們上面的例子中,memory barrier指令對store buffer和invalidate queue都進行了標注,不過,在實際的代碼片段中,foo函數不需要mark invalidate queue,bar函數不需要mark store buffer

因此,許多CPU architecture提供了弱一點的memory barrier指令只mark其中之一。

如果只mark invalidate queue,那么這種memory barrier被稱為read memory barrier。

相應的,write memory barrier只mark store buffer。

一個全功能的memory barrier會同時mark store buffer和invalidate queue。

我們一起來看看讀寫內存屏障的執行效果:

對於read memory barrier指令,它只是約束執行CPU上的load操作的順序,具體的效果就是CPU一定是完成read memory barrier之前的load操作之后,才開始執行read memory barrier之后的load操作。read memory barrier指令象一道柵欄,嚴格區分了之前和之后的load操作。

對於write memory barrier指令,它只是約束執行CPU上的store操作的順序,具體的效果就是CPU一定是完成write memory barrier之前的store操作之后,才開始執行write memory barrier之后的store操作。

全功能的memory barrier會同時約束load和store操作,當然只是對執行memory barrier的CPU有效。

現在,我們可以改一個用讀寫內存屏障的版本了,具體如下:

1 void foo(void)
2 {
3 a = 1;
4 smp_wmb(); //mark store buffer
5 b = 1;
6 }
7
8 void bar(void)
9 {
10 while (b == 0) continue;
11 smp_rmb(); // mark invalidate queue,
12 assert(a == 1);
13 }

有些CPU有更多種類的memory barrier操作,不過read mb,write mb和全功能的mb是應用普遍的指令,理解了這三個之后再學習其他的就比較簡單了。

參考文獻:

1、英文的原文來自perfbook-1c.2015.01.31a.pdf

2、翻譯的過程,參考了《深入理解並行編程V2.0.pdf》,多謝謝寶友/魯陽/陳渝的辛苦勞動。他們的翻譯忠於原著,我的翻譯都是滿嘴跑舌頭,_

轉發請注明出處。蝸窩科技 http://www.wowotech.net/kernel_synchronization/Why-Memory-Barriers.html

標簽: Memory 內存屏障 barrier


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM