Memory Barriers(内存屏障): a Hardware View for Software Hackers 阅读笔记
2017-01-08 11:22
357 查看
Memory Barriers: a Hardware View for Software Hackers(原文地址:http://www.puppetmastertrading.com/images/hwViewForSwHackers.pdf)是一篇介绍CPU缓存及内存屏障的原理,通过阅读这篇文章使我对CPU缓存工作原理和内存屏障有了新的认识,也让我对java内存模型有了新的理解,一下是本人关于这篇文章的前5章做的总结,限于本人的英语水平和相关知识的欠缺存在很多理解不正确的地方,欢迎大家指出。
1. 缓存结构
1.1 cache line结构(cache line)
- cache line为什么设计成多行两列
多行实际上是硬件hash结构,首先根据内存地址可以直接映射到某一cache line(比如地址0X…000–>0x0,0X…FO1–>OXF)。可以实现快速定位,否则需要逐行匹配,耗费时间
为什么需要设计成两列的模式
如果直接使用单列的硬件hash缓存结构,由于缓存数据经常会出现冲突,剔除原有缓存,造成缓存命中率大大降低。加上一列可以在出现冲突时有备用地址保存冲突元素
cache line每一次缓存多大的数据
cache line每一次缓存一块数据,比如cache line大小是64kb,则每次缓存是64kb数据。连续64kb的数据实际上映射到一个缓存块上去,比如0X00000002和0X00000003属于同一个缓存块,cache line其中一个都会缓存另外一个。
1.2 CPU总体缓存架构
缓存总体上由,缓存器,store buffer,invalidate queues三部分组成,其中缓存器用于存储缓存数据项store buffer用于存储cpu写入的数据项,invalidate queues用于保存接收到无效消息时,暂时存储的位置
2. 缓存协议
2.1 缓存状态MESI
缓存的主要状态有如下4种M:modified状态
表示当前cache line的数据项被修改过(未保存至内存),这种状态下该数据是被该CPU占有的,其他任何CPU中都不会存在有效的相同数据项。
E:exclusive状态
表示该数据被本CPU拥有,在其他CPU的缓存中都不存在对应的数据,这种情况下该CPU可以直接修改该数据项,而不需要发送消息
S:shared状态
表示该数据被多个CPU所共享(存在于多个CPU缓存中),这种情况下CPU不能直接修改该数据项
I:invalid状态
表示该数据无效,即删除,CPU加载该数据时不需要重新加载,不能直接使用
2.2 缓存协议消息
缓存消息主要有如下6种Read消息
该消息包含读的物理地址,一般用于加载数据
Read Respionse消息
该消息包含前面Read消息所请求的数据,可以由其他CPU缓存或者内存发出
Invalidate消息
该消息包含无效的物理地址,由某个CPU缓存发出,所有接收到消息的缓存需要移除对应的数据项(置无效)
Invalidate Acknowledge消息
在接收到Invalidate消息并移除对应数据后,相应的CPU缓存需要发送此消息
Read Invalidate消息
该消息包含读的物理地址,同时让其他CPU缓存移除对应数据。该消息可以接收到一个Read Response消息和一系列Invalidate Acknowledge消息
Writeback消息
该消息包含物理地址和需要被会写内存的数据,这个消息允许缓存为存放其他数据清除该数据所占的空间,否则该数据不能被移除
2.3 MESI状态转换
以下转换以CPU0的角度解释
(1)CPU0自身发送消息导致的状态转换
a:m->e
发送writeback消息,将数据写入内存,同时将当前数据项的状态置为Exclusive
b:e->m
不需要发送任何消息,CPU0拥有数据项,直接将数据写入cache line
j:i->e
CPU0意识到将要保存一个数据但是不在他的缓存中,CPU0发送read invalidate消息,在接收到一个read response消息和一系列validate acknowledge消息后,改变状态为exclusive,此时CPU0可以通过b保存数据项(也有说加载数据项后,发现其他缓存中不存在该数据,这种解释应该是错的)
d:i-m
CPU0执行一个原子性读写操作,直接保存一个数据项但是不在他的缓存中,CPU0发送read invalidate消息,在接收到一个read response消息和一系列validate acknowledge消息后,改变状态为modfied
k:i->s
CPU0发送read消息,加载数据项,得到read response消息后改变状态为shared
e:s->m
CPU0执行一个原子性读写操作,直接保存一个数据,但是该数据当前以shared(只读)状态在他的缓存中,CPU0发送invalidate消息,在接收到一系列validate acknowledge消息后,改变状态为modfied
h:s->e
CPU0意识到将要保存一个数据,但是该数据当前以shared(只读)状态在他的缓存中,CPU0发送invalidate消息,在接收到一个read response消息和一系列validate acknowledge消息后,改变状态为exclusive,此时CPU0可以通过b保存数据项;
或者所有其他CPU发送消息writeback将数据回写至内存(i-e可否知道自己单独存储?)
(2)其他CPU发送消息给CPU0导致的状态转换
c:m->i
CPU0接收到read消息,发送read response消息同时将modfied状态的数据项发送出去,改变自身状态置shared
f:m-s
CPU0接收到read消息,发送read response消息同时将modfied状态的数据项发送出去,改变自身状态置shared
i:e->i
其他CPU执行一个原子性读写操作,发送read invalidate消息,CPU0接收到消息后发送read response消息同时将modfied状态的数据项发送出去,然后发送invalidate acknowledge消息并将该数据项置invalidate
g:e->s
其他CPU读取一个数据项,发送read消息,CPU0接收消息后发送数据项,同时将其状态置为shared
l:s-i
其他CPU想要保存一个数据项,发送invalidate消息,CPU0接收消息后将该数据项状态置为invalidate
3. store buffer和内存屏障(memory barriers)
3.1 store buffer
(1)store buffer如何工作CPU0在写入共享数据时,直接将数据写入store buffer中,同时发送invalidate消息,等接收到所有的invalidate acknowledge消息时再将数据存储至cache line中
(2)为什么需要store buffer
虽然加上缓存后可以使数据的读取更加有效率,但是对于数据的存储来说却并不是非常有效率考虑如下:
CPU0存储数据项,发送invalidate消息,此时需要等待接收所有invalidate acknowledge消息才能将数据保存至cache line,这对存储来说是非常耗时的,而且CPU0总是会将数据存储至cache line中
(3)为什么需要让CPU可以从store buffer中加载数据
我们看一个例子,如下:
初始状态 CPU0拥有变量b=0,CPU1拥有变量a=0
CPU0执行a=1,cache misssing,发送read invalidate消息(为什么不是invalidate消息?)
CPU0直接存储a=1至store buffer中
CPU0接收CPU1发送的read response消息(a=0)
CPU0从cache line中载入a=0
CPU0接收invalidate acknowledge消息,将a=1写入cache line
CPU0执行b=a+1,其中a=0,且CPU0拥有b(状态为exclusive),所有直接写入cache line,并将b状态置为modified,此时b=1
CPU0执行assert(b==2)–>false
上述问题可以让CPU0直接加载a时直接读取store buffer中的a
3.2 内存屏障
(1)首先看一个例子初始状态CPU0拥有b=0,CPU1拥有a=0,CPU0执行foo,CPU1执行bar
CPU0执行a=1,CPU0缓存中不拥有a,所有将a=1放入store buffer,同时发送read invalidate消息
CPU1执行while(b==0)continue,CPU1缓存中不存在b,所以发送read消息
CPU0执行b=1,CPU0拥有b,所以直接写入缓存中(b状态为modfied)
CPU0接收到read消息,发送read response消息,同时将b状态编程shared
CPU1接收CPU0发送来的b=1的数据
CPU1结束while(b==0)的循环
CPU1执行assert(a==1),CPU1中a=0,所以FALSE
CPU1接收到read invalidate消息,发送a同时将a置invalid状态,此时已经晚了
CPU0接收到read response和invalidate acknowledge消息,将store buffer中的a=1存储至缓存中(a状态为modfied)
(2)上述问题的解决方案
使用内存屏障,该内存屏障主要使CPU简单的停止存储数据直到store buffer为空或者使用store buffer存储后续的数据直到先前的数据全部保存至缓存
代码如下(主要加上了内存屏障)
初始状态CPU0拥有b=0,CPU1拥有a=0,CPU0执行foo,CPU1执行bar
CPU0执行a=1,CPU0缓存中不拥有a,所有将a=1放入store buffer,同时发送read invalidate消息
CPU1执行while(b==0)continue,CPU1缓存中不存在b,所以发送read消息
CPU0执行smp_mp(),标记当期的store buffer中的数据项
CPU0执行b=1,CPU0拥有b,但是当前store buffer中有被标记的数据项(即上面的a),所有CPU0只能将b=1存储至store buffer
CPU0接收到read消息,发送read response消息,同时将b状态编程shared
CPU1接收CPU0发送来的b=0的数据
CPU1继续while(b==0)的循环,此时CPU1缓存中的b=0
CPU1接收到read invalidate消息,发送a同时将a置invalid状态
CPU0接收到read response和invalidate acknowledge消息,将store buffer中的a=1存储至缓存中(a状态为modfied)
CPU0的store buffer中只要a一个表标记,a被存储至缓存中,所以此时b也可以存储了,但是现在CPU0中b的状态是shared,所以CPU0发送invalidate消息
CPU1接收invalidate消息,发送acknowledge消息
CPU0接收acknowledge消息,将b状态置为exclusive,同时可以将store buffer中的b=1写入缓存中
CPU1执行while(b==0)但是此时CPU1缓存中不包含b,发送read消息
CPU0接收read消息,发送b=1,同时将b状态置为shared
CPU1接收CPU0发送来的b=1的数据
CPU1结束while(b==0)的循环
CPU1执行assert(a\==1),CPU1缓存中此时并不包含a,所以发送read消息,当其接收CPU0返回的a=1消息时,执行assert(a==1)此时正确
4. invalidate queues和内存屏障
4.1 invalidate queues
(1)invalidate queues如何工作CPU在接收到invalidate消息后,不用等到CPU真正将相应的缓存置为无效状态,CPU可以直接将对应数据加入invalidate queues中,同时直接发送invalidate acknowledge响应,当然在对应的数据被处理前,CPU不能再向其他CPU发送有关该数据的无效消息
(2)为什么需要invalidate queues
因为缓存存储器的容量有限(很小),CPU很容易将其填满(特别是加入了内存屏障的情况下),通过加入invalidate queues可以让其他CPU快速响应acknowledge消息,以将数据从store buffer保存至缓存器中
4.2 内存屏障
(1)首先看一个例子这个例子还是使用3.2(1)中的代码
初始状态CPU0拥有b=0(独有),存有a=0(shared),CPU1存有a=0(shared),CPU0执行foo,CPU1执行bar
CPU0执行a=1,CPU0缓存a的状态是shared,将a=1放入store buffer,同时发送invalidate消息
CPU1执行while(b==0)continue,CPU1缓存中不存在b,所以发送read消息
CPU0执行b=1,CPU0拥有b,所以直接将其存储至缓存中
CPU0接收到read消息,发送read response消息,同时将b状态变成shared
CPU1接收invalidate消息,将其加入invalidate queues,同时直接发送invalidate acknowledge消息(此时CPU1任然拥有a)
CPU1接收CPU0发送来的b=1的数据
CPU1结束while(b==0)的循环
CPU1执行assert(a==1),但是此时a的值为0,因为CPU1的缓存中还存在a
CPU1处理invalidate queues,将a移除缓存,但是此时已经晚了
CPU0接收到invalidate acknowledge消息,保存a=1至缓存中
(2)上述问题的解决方案
使用内存屏障,该内存屏障主要将invalidate queues中的数据项都标记,CPU后续的加载数据都必须等到这些被标记的数据处理完毕
代码如下:
初始状态CPU0拥有b=0(独有),存有a=0(shared),CPU1存有a=0(shared),CPU0执行foo,CPU1执行bar
CPU0执行a=1,CPU0缓存a的状态是shared,将a=1放入store buffer,同时发送invalidate消息
CPU1执行while(b==0)continue,CPU1缓存中不存在b,所以发送read消息
CPU0执行b=1,CPU0拥有b,所以直接将其存储至缓存中
CPU0接收到read消息,发送read response消息,同时将b状态变成shared
CPU1接收invalidate消息,将其加入invalidate queues,同时直接发送invalidate acknowledge消息(此时CPU1任然拥有a)
CPU1接收CPU0发送来的b=1的数据
CPU1结束while(b==0)的循环
CPU1执行smp_mb内存屏障,将invalidate queues中的数据标记
CPU1执行assert(a==1),但是此时a被内存屏障标记,存在于invalidate queues中,所有不能加载a直至invalidate queues中a的消息被处理了
CPU1处理invalidate queues,将a移除缓存
CPU1此时可以加载a了,但是此时CPU1中并不包含a,所有发送read消息
CPU0接收到invalidate acknowledge消息,保存a=1至缓存中
CPU0接收read消息,将a=1发送至CPU1
CPU1接收CPU0返回的a=1消息,执行assert(a==1)此时正确
5. 读写内存屏障
上述例子中的内存屏障会同时处理store buffer和invalidate queues,但是在我们的代码中foo并不需要处理invalidate queues,同样的bar也无需处理store buffer,所以一些CPU架构将两者分开处理,分别是读内存屏障和写内存屏障如下:写内存屏障主要解决写入数据至缓存存储器时,保证后续的写操作不能再这个写操作之前(也就是内存指令不能重排),这样可以避免CPU0执行a=1(写入store buffer),b=1(直接写入),然后其他CPU读取CPU0数据时,造成读到了b=1的操作,而未得到a=1的导致的结果,通过内存屏障强制a=1发生在b=1之前
读内存屏障主要解决invalidate queues中的数据和缓存中数据的冲突问题,保证CPU读取数据时必须先执行完invalidate queues中的任务,否则CPU0执行a=1,b=1操作时,CPU1可能读取到的a还是a=0(因为a=1发送的无效消息,但是该消息并不直接将CPU1缓存中的a置为无效),但是b=1(直接从CPU0读取
相关文章推荐
- web性能优化之:no-cache与must-revalidate深入探究
- 页面缓存:内存和文件之间的那些事
- IE7降低内存和降低CPU的几个技巧
- 如何高效的使用内存
- DOS下内存的配置
- XP/win2003下发现1G的内存比512M还慢的解决方法
- 浅析SQL Server中的执行计划缓存(上)
- Enterprise Library for .NET Framework 2.0缓存使用实例
- PowerShell中编程清空IE缓存方法
- PowerShell中使用.NET将程序集加入全局程序集缓存
- PowerShell实现动态获取当前脚本运行时消耗的内存
- C#实现把dgv里的数据完整的复制到一张内存表的方法
- SQL语句实现查询SQL Server内存使用状况
- C#中缓存的基本用法总结
- mysql缓冲和缓存设置详解
- C语言内存对齐实例详解
- C++ 类中有虚函数(虚函数表)时 内存分布详解
- 深入学习C语言中memset()函数的用法
- C++对象内存分布详解(包括字节对齐和虚函数表)
- 浅谈C++对象的内存分布和虚函数表