CPU, Memory Barrier, Cache Coherence

乱序也分读/写,在 x86 IA32 这种体系下只有“读乱顺”,而没有“写乱序”,
在 x86 IA32 下“写”是严格顺序的。对于软件来说,为了保证 CPU流水线的
效率提升不导致混乱,确保 process ordering 与 program ordering 的一致
性问题,而使用了“compiler barrier”,“memory barrier”(下面会讲到),
在某些平台下,如 WINDOWS VISTA 与 WINDOWS 7 以前的版本 “memory barrier”
的实现基本是发送 lock# 信号到北桥,由它来锁住 host BUS。关于这点可以查
看早期 WINDOWS 的 KeMemoryBarrier() 函数,其基本上就是一条 xchg 指令
(熟悉 x86 IA32 的人都知道,即使没有 lock 前缀,该指令一样会发送 lock#
信号)。

在 x86 IA32 下,可以说所有 “memory barrier” 特有指令能做的事,lock
指令同样能做到。但这并不表明就应该使用 lock 指令来替代“memory barrier”
特有指令。我们都知道在 SMP 下 spinlock 的实现就是基于 lock 指令的,而使
用这种粗粒度的指令来完成“memory barrier” 结果就是效率上的损失。尤其是
在 SMP 上,由于 x86 IA32 使用的 Cache Snooping 协议,当 CPU-A 发送 lock#
信号锁住 host BUS,即 MRM 占用 host BUS,此时 CPU-B 即 LRM 的 PHIT 和
PHITM 对 MRM 进行监听,若 MRM 操作命中 LRM 某有效 CACHE LINE 则 PHIT 受
信,若命中修改状态的某有效 CACHE LINE 则 PHITM 受信,接下来 MRM 就会
INVALIDATE 此 CACHE LINE。(P6 后的实现有所不同,lock# 不会锁 host BUS,
而是用 CACHE lock 实现,这样虽然比锁 host BUS 效率高但仍不是好的解决方案)
在使用 xFENCE 等指令来实现“memory barrier”的情况下,要比使用 lock 指令
效率高很多。其实在 PIII 后就已经提供了 sfence、lfence、mfence 这些专用于
“memory barrier”的指令,这些指令在 WINDOWS VISTA 与 WINDOWS 7 中可见。
可能是为了考虑通用与一致性问题,这些指令在以前的版本中没有使用。

前面说过在 x86 IA32 下“写” 是严格顺序的。但其他 RISC 平台上的开发就没
那么幸运了,这些问题必须自己考虑,我在 PWOER CPU 的 AIX Kernel 下就碰到
过这种问题。还有在 EPIC 下,需要做的还有很多。

上面提到的 “compiler barrier”,“memory barrier”。先看下 “compiler
barrier” 的具体应用,现代编译器通常会充分利用该体系 CPU 的乱序功能来进
行优化。在编程中定义一个变量时,如果不想让编译器对其进行乱序优化可以使
用 volatile 关键字。如: volatile int *p1; 这样则基本上可以保证编译出的
代码没有对其进行乱序处理。可以反汇编验证如下代码的区别。

int *p1;  
volatile int *p1 //以不同方式定义 p1;

int *p2;
......// 略掉一系列为 p1,p2 分配内存等操作。
*p1 = 1;
*p1 = 2;
*p2 = *p1;

但这仅是保证编译出来的代码不会乱序。再来看下 “memory barrier” 的具体
应用。首先要知道 CPU 在什么情况下才会乱续执行?什么样的代码不会被乱续
执行?以下面的结构为例。

typedef struct _barrier{
     int  n1;
     int  n2;
}barrier;

先看如下代码。CPU 是的乱序执行是不会先执行后一条指令的。也就是说此代码
的 mybarrier.n2 = mybarrier.n1; 不会在 mybarrier.n1 = 0; 之前执行,因为
CPU 的预测执行会检测到代码的依赖关系,此类依赖称之为 WAW(write after
write )依赖(还有 RAW,WAR 依赖)。所以无须添加“memory barrier”。

barrier mybarrier;
mybarrier.n1 = 0;
mybarrier.n2 = mybarrier.n1;

再看改动后的代码前,设想以下情景,假设有一块 PCI/PCIE 设备,你为其写
DRIVER。在通过 PCI 的 BDF 得到其 PCI Configuration Space 后,根据 PCI BAR
的最后一位为 0,表示为 MMIO。此时,你将这个状态记录到你的结构中去如:
“mybarrier.n1 = 1;”,然后设置“mybarrier.n2 = 0;” 表示设备准备就绪,
可以进行操作。

mybarrier.n1 = 1;
mybarrier.n2 = 0;
......
......

CPU 的乱序预测很可能会先执行 n2 = 0。然后再执行 n1 = 1; 如果这时你取记录
n1 很可能得到的是 0,从而误认为你的 PCI 设备是 PORT I/O 方式。也就是说如
果你的代码是要根据前一条指令的结果来决定后一条指令,那么这样编写就很有可
能出现问题。说白了就是在 CPU 看来两个不相关的变量地址读/写会预测乱序执行。
为了保证代码不乱序执行才提出了 “memory barrier”内存屏障的概念。也就是
在你的代码中要显示的加入一些函数来保证执行时的顺序。此类指令最终是与体系
相关的,在不同平台上所用的汇编指令不同。如 POWER AIX 上的
“memory barrier”则是由 sync, eieio, lwsync 等指令实现。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值