缓存一致性和访存顺序的区别

来源:互联网 发布:windows编程书籍 编辑:程序博客网 时间:2024/06/13 08:13
感觉有人把访存次序consistency和缓存一致性coherency搞混了。我从硬件角度说说他们到底是什么。
首先我有一些假设,每个核拥有独立的一级缓存,他们之间通过总线相连并一直保持开启和一致性。为了简单起见我把二三级省略了。

缓存一致性,排名第一的答案解释的很好了,我只加一点,就是它是针对同一缓存行的地址的。有些变量处于相同缓存行的不同字节,我们仍然认为他们是一个地址。

而访存次序定义了访存指令执行的次序。再详细一点,访存可以分成两部分,一部分是请求request,一部分是完成completion,这两步是可以拆开的。并且,请求的次序和完成的次序可以是打乱的。例如read A, read B可以拆分成read A req, read B req, read A comp, read B comp或者read A req, read A comp, read B req, read B comp,又或者read B req, read A req, read A comp, read B comp或者read B req, read B comp, read A req, read A comp。 

我们先看一个核的情况。当连续的两个读read A, read B被发到总线,我们想保持请求或完成有序,那么就插入内存壁垒指令mbar。内存壁垒指令又可以分为两类,一类是弱壁垒mbar0,一类是强壁垒mbar1。强壁垒是说,我必须等第一个读完成了,才能发第二个。也就是read A req, read A comp, mbar1, read B req, read B comp。弱壁垒只需要保证请求有先后次序就行,read A req, mbar0, read B req, read A comp, read B comp或者read A req, mbar0, read B req, read B comp, read A comp。它们对应的指令都是read A, mbar, read B。

在之后的部分,我假设mbar都是强壁垒,而不加mbar的时候,请求和完成次序完全是乱的,这样便于分析。

我们在一个核上引入写,write A, mbar, read B。当地址A=B,此时能保证读到的是写入的值。不加这个mbar我就没法保证,加了缓存也没用,因为写可能都没发生。
当A和B不相等,也有特殊的用途。假设我访问的是设备的寄存器,先要设一个寄存器让他开始工作,然后再去读另一个寄存器看看状态,这时候就必须要壁垒指令,否则读状态的时候设备还没开始工作。当然,此时一般对于设备的缓存是禁止的,并且不可推测执行,防止额外的访问。例外的情况是有些处理器如powerpc,关掉缓存就默认是强制有序,所以不需要壁垒指令。


当我们把访存次序把缓存一致性结合考虑,可以假设在C和D两个核上,同时循环跑write A coreC, read B coreC和write A coreD, read B coreD。核C执行write A coreC时,总线会广播到核D,看看是不是有同样的地址,做一系列操作。具体过程请参考mesi协议,我就不详细写了。有人说每次都这样广播岂不是很浪费时间,没错。优化方法之一是把缓存在总线里存个小副本,只保留状态和源地址,这样就不必总是到另外一个核去查了。这时候不管单核和双核,读写次序都是乱的,完全没法做任何保证。唯一能保证的是缓存一致性。

然后我们改下代码,加入壁垒write A coreC, mbar, read B coreC和write A coreD, mbar, read B coreD。这时候核之间次序没保证,每个核上的有次序保证。可能发生的序列是write A coreC, write A coreD, read B coreD, read B coreC.

假设地址A和B相同,此时核C读到的可能是核D写入的值,而不是他自己写入的。此时缓存一致性还是没有问题,但是却未必是我们想要的结果。

所以在多核时,哪怕使用了内存壁垒指令,也没法保证读写的原子性。解决办法有两个,一个是软件锁,一个是原子操作。原子操作我看到过的有两种,一种是总线收到锁请求时,直接封掉整个总线,同时只有一个核能访问。这样效率很低。还有个方法是把锁的请求发送到对端设备,比如内存控制器,让他禁止别的核的访问,而总线依然可以运行,这样效率就高不少,我看到过的数据,减少10倍时间。
软件锁我说一个自旋锁,因为它能用一个硬件机制来完美模拟。当使用特殊指令对一个地址写入值,相应缓存行上会做一个特殊标记,表示还没有别的核去写这行缓存。然后下条指令读这个行,如果标记没变,说明写和读之间没有人打扰,那么就拿到锁了。如果变了,那么回到写的过程重新获取锁。当然,过程中可能还是需要壁垒指令来保证次序。

更复杂的情况,我们可以把访存的请求和完成,弱壁垒和强壁垒同时考虑,来达到最大的访问效率。缓存一致性也可以单向来用,形成snoop.

希望能有所帮助。
0 0
原创粉丝点击