讀者朋友你好hello緩存
在開始閱讀以前咱們假設讀者已經掌握了緩存一致性協議的MESI相關知識。若是沒有建議閱讀 帶你瞭解緩存一致性協議 MESI安全
問題的產生微信
如上圖 CPU 0 執行了一次寫操做,可是此時 CPU 0 的 local cache 中沒有這個數據。因而 CPU 0 發送了一個 Invalidate 消息,其餘全部的 CPU 在收到這個 Invalidate 消息以後,須要將本身 CPU local cache 中的該數據從 cache 中清除,而且發送消息 acknowledge 告知 CPU 0。CPU 0 在收到全部 CPU 發送的 ack 消息後會將數據寫入到本身的 local cache 中。這裏就產生了性能問題:當 CPU 0 在等待其餘 CPU 的 ack 消息時是處於停滯的(stall)狀態,大部分的時間都是在等待消息。爲了提升性能就引入的 Store Buffer。函數
Store Buffer工具
store buffer 的目的是讓 CPU 再也不操做以前進行漫長的等待時間,而是將數據先寫入到 store buffer 中,CPU 無需等待能夠繼續執行其餘指令,等到 CPU 收到了 ack 消息後,再從 store buffer 中將數據寫入到 local cache 中。有了 store buffer 以後性能提升了許多,但常言道:「有一利必有一弊。」store buffer 雖然提升了性能可是卻引入了新的問題。oop
a = 0 , b = 0; a = 1; b = a + 1; assert(b == 2);
假設變量 a 在 CPU 1 的 cache line 中 , 變量 b 在 CPU 0 的 cache line 中。上述代碼的執行序列以下:性能
1. CPU 0 執行 a = 1。spa
2. CPU 0 local cache 中沒有 a ,發生 cache miss 。設計
3. CPU 0 發送 read invalidate 消息獲取 a ,同時讓其餘 CPU local cache 中的 a 被清除。code
4. CPU 0 把須要寫入 a 的值 1 放入了 store buffer 。
5. CPU 1 收到了 read invalidate 消息,迴應了 read response 和 acknowledge 消息,把本身 local cache 中的 a 清除了。
6. CPU 0 執行 b = a + 1 。
7. CPU 0 收到了 read response 消息獲得了 a 的值是 0 。
8. CPU 0 從 cache line 中讀取了 a 值爲 0 。
9. CPU 0 執行 a + 1 , 並寫入 b ,b 被 CPU 0 獨佔因此直接寫入 cache line , 這時候 b 的值爲 1。
10. CPU 0 將 store buffer 中 a 的值寫入到 cache line , a 變爲 1。
11. CPU 0 執行 assert(b == 2) , 程序報錯。
致使這個問題是由於 CPU 對內存進行操做的時候,順序和程序代碼指令順序不一致。在寫操做執行以前就先執行了讀操做。另外一個緣由是在同一個 CPU 中同一個數據存在不一致的狀況 , 在 store buffer 中是最新的數據, 在 cache line 中是舊的數據。爲了解決在同一個 CPU 的 store buffer 和 cache 之間數據不一致的問題,引入了 Store Forwarding。store forwarding 就是當 CPU 執行讀操做時,會從 store buffer 和 cache 中讀取數據, 若是 store buffer 中有數據會使用 store buffer 中的數據,這樣就解決了同一個 CPU 中數據不一致的問題。可是因爲 Memory Ordering 引發的問題尚未解決。
內存操做順序
Memory Ordering
a = 0 , b = 0; void fun1() { a = 1; b = 1; } void fun2() { while (b == 0) continue; assert(a == 1); }
假設 CPU 0 執行 fun1() , CPU 1 執行 fun2() , a 變量在 CPU 1 cache 中 , b 變量在 CPU 0 cache 中。 上述代碼的執行序列以下:
產生問題的緣由是 CPU 0 對 a 的寫操做尚未執行完,可是 CPU 1 對 a 的讀操做已經執行了。畢竟CPU並不知道哪些變量有相關性,這些變量是如何相關的。不過CPU設計者能夠間接提供一些工具讓軟件工程師來控制這些相關性。這些工具就是 memory barrier 指令。要想程序正常運行,必須增長一些 memory barrier 的操做。
寫內存屏障
Store Memory Barrier
a = 0 , b = 0; void fun1() { a = 1; smp_mb(); b = 1; } void fun2() { while (b == 0) continue; assert(a == 1); }
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以後才操做。增長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中,所以,CPU發送一個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變量賦值的操做,都須要那麼長的執行過程,並且每一步都須要芯片參與,最終完成整個複雜的賦值操做過程。
上述這個例子展現了 write memory barrier , 簡單來講在屏障以後的寫操做必須等待屏障以前的寫操做完成才能夠執行,讀操做則不受該屏障的影響。
順序寫操做致使了 CPU 的停頓
Store Sequences Result in Unnecessary Stalls
按照矛盾的角度來看解決了一個問題以後伴隨着又產生了一個新的問題:每一個cpu的store buffer不能實現的太大,其entry的數目不會太多。當cpu以中等的頻率執行store操做的時候(假設全部的store操做致使了cache miss),store buffer會很快的被填滿。在這種情況下,CPU只能又進入等待狀態,直到cache line完成invalidate和ack的交互以後,能夠將store buffer的entry寫入cacheline,從而爲新的store讓出空間以後,CPU才能夠繼續執行。這種情況也可能發生在調用了memory barrier指令以後,由於一旦store buffer中的某個entry被標記了,那麼隨後的store都必須等待invalidate完成,所以不論是否cache miss,這些store都必須進入store buffer。爲了解決這個問題引入了 invalidate queues 能夠緩解這個情況。store buffer之因此很容易被填充滿,主要是其餘CPU迴應invalidate acknowledge比較慢,若是可以加快這個過程,讓store buffer儘快進入cacheline,那麼也就不會那麼容易填滿了。
invalidate acknowledge不能儘快回覆的主要緣由是invalidate cacheline的操做沒有那麼快完成,特別是cache比較繁忙的時候,這時,CPU每每進行密集的loading和storing的操做,而來自其餘CPU的,對本CPU local cacheline的操做須要和本CPU的密集的cache操做進行競爭,只要完成了invalidate操做以後,本CPU纔會發生invalidate acknowledge。此外,若是短期內收到大量的invalidate消息,CPU有可能跟不上處理,從而致使其餘CPU不斷的等待。
然而,CPU其實不須要完成invalidate操做就能夠回送acknowledge消息,這樣,就不會阻止發生invalidate請求的那個CPU進入無聊的等待狀態。CPU能夠buffer這些invalidate message(放入Invalidate Queues),而後直接回應acknowledge,表示本身已經收到請求,隨後會慢慢處理。固然,再慢也要有一個度,例如對a變量cacheline的invalidate處理必須在該CPU發送任何關於a變量對應cacheline的操做到bus以前完成。
有了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協議消息。
讀內存屏障
Load Memory Barrier
a = 0 , b = 0; void fun1() { a = 1; smp_mb(); b = 1; } void fun2() { while (b == 0) continue; assert(a == 1); }
假設 a 存在於 CPU 0 和 CPU 1 的 local cache 中,b 存在於 CPU 0 中。CPU 0 執行 fun1() , CPU 1 執行 fun2() 。操做序列以下:
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執行assert(a == 1),可是因爲這時候CPU 1 cache的a值仍然是舊值0,所以assert 失敗。
10. Invalidate Queue中針對a cacheline的invalidate消息最終會被CPU 1執行,將a設定爲無效。
很明顯,在上面場景中,加速 ack 致使fun1()中的memory barrier失效了,所以,這時候對 ack 已經沒有意義了,畢竟程序邏輯都錯了。怎麼辦?其實咱們可讓memory barrier指令和Invalidate Queue進行交互來保證肯定的memory order。具體作法是這樣的:當CPU執行memory barrier指令的時候,對當前Invalidate Queue中的全部的entry進行標註,這些被標註的項次被稱爲marked entries,而隨後CPU執行的任何的load操做都須要等到Invalidate Queue中全部marked entries完成對cacheline的操做以後才能進行。所以,要想保證程序邏輯正確,咱們須要給 fun2() 增長內存屏障的操做,具體以下:
a = 0 , b = 0; void fun1() { a = 1; smp_mb(); b = 1; } void fun2() { while (b == 0) continue; smp_rmb(); assert(a == 1); }
當 CPU 1 執行完 while(b == 0) continue; 以後, 它必須等待 Invalidate Queues 中的 Invalidate 變量 a 的消息被處理完,將 a 從 CPU 1 local cache 中清除掉。而後才能執行 assert(a == 1)。CPU 1 在讀取 a 時發生 cache miss ,而後發送一個 read 消息讀取 a ,CPU 0 會迴應一個 read response 將 a 的值發送給 CPU 1。
許多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有效。
看完本文有收穫?請分享給更多人
微信關注「黑帽子技術」加星標,看精選 IT 技術文章