深入理解内存屏障

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);
}

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