深入理解內存屏障

perfbook Appendix C 章節的翻譯,感覺這本書講內存屏障講的非常好,以下只是該章節部分英文翻譯,想深入理解的可以看看書。

MESI state

M(modify) : CPU擁有該cache line,這個cache line內包含最新的數據,這個cache負責最終寫回內存或者傳遞數據到其他cpu cache
E(exclusiv) : 和modify狀態相似,唯一的區別是沒有被該CPU修改,和內存中的保持一致.這個CPU可以store data不需要和其他CPU交流
S(Shared) : read only, 至少還有一個CPU的cache有它的相同,和內存的數據也相同
I(invalidate) : empty, it hold no data

MESI protocol 消息類型

Read:
Read消息包含被讀cache line的物理地址
Read Response:
回覆read消息,內存和其他CPU cache都可以提供該回復消息
如果一個cache line的狀態是Modify,這個cache必須提供read response
Invalidate;
包含被無效cache line的物理地址,其他cpu caches 必須移除相應數據並給出響應
Invalidate Acknowledge
收到Invalidate的CPU 必須響應該類型消息
Read Invalidate:
包含被讀cache line的物理地址,同時告訴其他cpu cache 無效數據
它是read he invalidate的結合,必須收到read respond 和 incalidate acknowledge回覆.
Writeback:
寫回內存
該消息允許高速緩存根據需要彈出處於“修改”狀態的行,以便爲其他數據騰出空間。

MESI 狀態轉換表:
MESI state

解釋:
a(M–>E)
一個cache line 被寫回內存,並且該CPU還保留cache line在自己的cache中,未來還有權限修改它.
這個轉換需要Writeback消息
b(E–>M)
CPU寫一個狀態已經是exclusive的cache line
這個轉換不需要接受或者發送消息
c(M–>I)
CPU收到它已經修改的某一cache line的read invalidate消息, CPU必須無效他的本地copy,
併發送read respond和invalidate ackknowledge
d(I–>M)
CPU對在他自己的cache上不存在的內存做一個原子的read-modify-write操作,
發送read invalidate
等待接收相對應的respond
e(S–>M)
CPU對在他自己的cache上存在的內存做原子read-modify-write操作
發送invalidate消息,並等待迴應
f(M–>S)
其他cpu 讀取cache line,該CPU將數據傳遞給其他CPU
當CPU接收read message時,狀態轉化發生,read respond被髮送。 有可能也會協會內存
g(E–>S)
其他CPU read cache line,數據可以從內存或者這個CPU提供
收到read,並回復read respond,狀態轉化
h(S–>E)
CPU意識到他將向這個cache line寫入數據,因此發送invalidate消息,等待回覆並修改狀態
i(E–>I)
其他CPU發送了原子讀-修改-寫操作,修改狀態後該CPU回覆消息
j(I–>E)
CPU對不在cache line中存在的數據做store, 發送了read invalidate, 收到響應後,狀態轉換
k(I–>S)
CPU發送read消息,該cache line在其他CPU內
l(S–>I)
其他CPU做了store動作,該CPU收到invalidate消息,狀態轉化

Store 導致不必要的等待
當CPU的某一cache line處於Modify或者exclusive狀態時,store指令會將數據很快寫入cache line內,
但是當CPU的cache line處於Shared或者Invalidate狀態時,srote指令寫入數據之前必須等待其他CPU的響應
爲了避免這種不必要的等待,CPU內引入了store buffer
即當CPU需要等待響應時,現在可以直接將數據寫入store buffer,然後繼續執行下一條指令,等到收到響應後,再將store buffer中的數據寫入cache line

在這裏插入圖片描述

Store Buffer的引入帶來並行編程的問題
考慮以下的情況:
a=1
b=a+1
assert(b == 2)
a b 被初始化爲0,a在CPU1的某一cache line上,b在CPU0的另一cache line上

  1. CPU0開始執行a=1,
  2. CPU0 cache missing,
  3. CPU0發送read invalidate,爲了獲得cache line(包含a)的獨有權限
  4. CPU0將a=1存儲到store buffer
  5. CPU1收到read invalidate, 發送a=1 read respond,無效cache line,發送invalidate ack
  6. CPU0執行b=a+1=1
  7. CPU0收到響應,load a=0 到cache line
  8. CPU0 將store buffer內數據存儲到 cache line (a=1)
  9. CPU0 執行assert失敗

Memory Barries
第二個例子
void foo(void){
a=1;
b=1
}

void bar(void){
while(b ==0);
assert(a ==1);
}

假設CPU0執行foo,CPU1執行bar,包含a的cache line在CPU1的cache,包含b的cache line 在CPU0的cache;執行序列如下

  1. CPU0執行a=1; cpu0 cache miss; 發送read invalidate
  2. CPU1執行while(b==0); CPU1 cache miss; 發送read
  3. CPU0 執行b=1; 因爲它已經擁有了b的cache line,因此store b=1 to cache line
  4. CPU0 收到read,發送b=1 to CPU1;標記cache line state爲shared
  5. CPU1 收到read響應,insatll b=1的cache line到自己的caches
  6. CPU1 跳出while循環
  7. CPU1 執行assert(a==1); CPU1仍然包含a=0的cache line,因此assert fail
  8. CPu1 收到read invalidate,並且invalidate自己的cache line,但是太遲了
  9. CPU0 收到CPU1的read 和 invalidate的響應,將store buffer中的a=1存儲到cache line

CPU並不知道a和b是相關的,爲了允許軟件告訴CPU這樣相關性,CPU設計者提供了memory barrier指令,內存屏障smp_mb會導致每個後續store指令應用於其變量的cache line之前先刷新它的store buffer.CPU可以簡單的停止直到store buffer在繼續操作之前爲空,或者可以使用store buffer來保存後續的stores指令,直到store buffer內的所有先前條目被寫入cache lineCPU可以簡單的停止直到store buffer在繼續操作之前爲空,或者可以使用store buffer來保存後續的stores指令,直到store buffer內的所有先前條目被寫入cache line

加入內存屏障
void foo(void) {
a=1;
smp_mb();
b=1;
}

void bar(void) {
whild(b == 0);
assert(a == 1);
}

假設CPU0執行foo,CPU1執行bar,包含a的cache line在CPU1的cache,包含b的cache line 在CPU0的cache;執行序列如下

  1. CPU0執行a=1; cache miss; 發送read invalidate
  2. CPU1執行while(b==0); CPU1 cache miss; 發送read
  3. CPU0執行smp_mb(); mark 當前所有的store buffer條目(即 a=1)
  4. CPU0 執行b=1; 因爲它已經擁有了b的cache line,本應store b=1 to cache line
    但是由於store buffer有被標記的條目; 因此store b=1 to store buffer
  5. CPU0 收到read,發送b=0 to CPU1;標記cache line state爲shared
  6. CPU1 收到read響應,insatll b=0的cache line到自己的caches
  7. 由CPU1內b仍然爲0; 重複while
  8. CPu1 收到read invalidate,發送a=0給CPU0;並且invalidate自己的cache line
  9. CPU0 收到a的cache line內容,並將store buffer的值應用於cache line,狀態變爲modified
    10.(這一步只是說明???) 因爲store buffer內的條目只有a的被smp_mb標記,現在a已經store到cache line
    因此,CPU0現在也可以將b的store buffer應用於對應的cache line ;但cache line b現在處於shared狀態
  10. CPU0 發送invalidate( cache line b) to CPU1
  11. CPU1收到invalidate; 無效b;發送ACK
  12. CPU1 執行b==0; cache miss; 發送read
  13. CPU0 收到ACK; CPU0 將b的新值存儲到cache line b; 設置b的cache line到exclusive狀態
  14. CPU0 收到收到read; 發送b的cache line to CPU1; 並將cache line的狀態改爲shared
  15. CPU1 收到cache line b and 存儲到 cache
  16. CPU1 執行while跳出
  17. CPU1 執行assert(a==1); cache miss
  18. … …

Store Sequences 導致不必要的等待
當CPU收到read invalidate後,往往需要很長時間才能確認相應的cache line是invalidate的,如果cache很忙,例如CPU正在緊密的進行load和store,這將還會更加延遲;
然而實際上CPU在發送ACK之前不需要invalidate cache line.相反,它可以使用invalidate queue; 但要了解該消息將在CPU發送有關該cache line的任何其他消息之前進行處理

就是說CPU收到read invalidate後不需要等cache line真正invalidate後再發送ACK,現在只要將消息放入invalidate queue,就可發送ACK,CPU後續會處理這個invalidate queue
在這裏插入圖片描述

Invalidate Queues and Memory Barriers
Invalidate Queues的引入也帶來了和store buffer類似的問題

void foo(void) {
a=1;
smp_mb();
b=1;
}

void bar(void) {
whild(b==0);
assert(a == 1);
}

假設CPU0執行foo,CPU1執行bar,包含a的cache line在CPU1和CPU0的cache,包含b的cache line 在CPU0的cache;執行序列如下:

  1. CPU0執行a=1; 因爲cache state爲shared; 因此store a=1 to store buffer; 發送invalidate
  2. CPU1 執行b==0;cache miss; read
  3. CPU1 收到invalidate ; 加入到invalidate隊列; 立即發送ACK
  4. CPU0 收到響應; 因此可以隨意通過smp_mb()指令; a=1存儲到store buffer
  5. CPU0 執行b=1; store b to cache line
  6. CPU0 收到read b;發送b=1
  7. CPU1 收到b=1; 放入cache line
  8. CPU1 跳出while
  9. CPU1 執行assert(a==1); a=0仍然在CPU1的cache內;assert failed
  10. CPU1 處理invalidate queue; invalidate cache line a

使用smb_mb解決
當CPU執行內存屏障指令時,它不僅可以標記store buffer; 還可以將當前在其invalidate queue中的所有條目;並強制所有後續的load指令等待;直到所有條目都應用到CPU cache line.

因此可以在load指令之間加入smp_smb()
void foo(void) {
a=1;
smp_mb();
b=1;
}

void bar(void) {
whild(b==0);
smp_mb();
assert(a == 1 );
}

  1. CPU0執行a=1; 因爲cache state爲shared; 因此store a=1 to store buffer; 發送invalidate
  2. CPU1 執行b==0;cache miss; read
  3. CPU1 收到invalidate ; 加入到invalidate隊列; 立即發送ACK
  4. CPU0 收到響應; 因此可以隨意通過smp_mb()指令; a=1存儲到store buffer
  5. CPU0 執行b=1; store b to cache line
  6. CPU0 收到read b;發送b=1
  7. CPU1 收到b=1; 放入cache line
  8. CPU1 跳出while
  9. CPU1 執行smp_mb; 現在必須停止,直到invalidate queue中的所有條目執行完畢
  10. … …

Read and Write Memory Barriers
內存屏障既可以標記store buffer 也可以標記invalidate queue;但是在foo的代碼段裏;沒有必要標記invalidate queue;在bar的代碼段裏;沒有必要標記store buffer
因此許多CPU架構提供了弱內存屏障僅僅標記其中之一;
read memory barrier --> invalidate queue
write memory barrier --> store buffer
void foo(void)
{
a = 1;
smp_wmb();
b = 1;
}

void bar(void)
{
while (b == 0) continue;
smp_rmb();
assert(a == 1);
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章