浅谈Memory barrier

浅谈Memory barrier

1. 内存乱序

我们常规思维下,代码的执行是按照我们撰写的顺序来执行的,类似如下的方式:

1. 读取程序计数器PC指向的指令
2. PC指向下一条指令
3. CPU执行当前指令
4. 重复上述过程直到程序执行完成

这是很原始的,类比小学课本里面的泡茶问题,我们泡茶有几个步骤:洗水壶、烧开水、洗茶壶、洗杯子、拿茶叶、泡茶;聪明的孩子一般都不会顺序执行,合理的方式是在不违背处理的先后逻辑的基础上,适当打乱操作顺序,压缩执行所需要的时间。这种改变执行顺序的方式叫做乱序执行,也叫memory reordering,内存乱序。

CPU有两类操作,计算操作与读写内存操作,例如下面简单的例子,语句1主要是一个计算操作,CPU执行的速度很快,语句2执行过程中,需要从静态内存区读取字符串常量,读取内存的过程中,CPU的等待周期远大于计算周期;因而同样一条语句,两者的执行时间显然不同。那么编译器、CPU就会根据这些特点,打乱指令的执行顺序。分别是编译期重排和运行期重排。因此效率更高的方式是先把没有数据依赖的操作提前进行(当PC指针还没变动时),等实际要执行这条指令的时候就可以快速得到结果。这就和小学时候的如何最快泡茶问题殊途同归了。但是实际上的代码指令的调整要复杂的多,很难进行预测。

// 1
a = 3*3;
// 2
b = "string";

为了降低读写内存带来的时间消耗,自然而然的引入了寄存器、多级缓存的技术,通过将使用概率更高的数据放入缓存的方式,减轻对主存的读写需求,对应的概念就是内存命中率,也就是程序的内存连续性;举个例子,程序运行时,会一次性加载一段连续的内存到缓存中,那么在读取一段连续的内存的数据的时候,极有可能会在下一个缓存位置命中我们所需的数据,避免再次读写主存,相反,如果读取数据的随机性、跨度较大,执行过程中就会频繁的读主存,刷新缓存,效率就会很低。C++的对象模型的设计就尽可能的遵守了这样的原则。

在单核单线程的情况下,这种背后暗戳戳的优化不会有任何有感的缺陷,但是在多核多线程的场景中,问题就复杂了。有两个因素:

  • 缓存未必是共享的,每个核有自己独有的缓存;
  • 每个核执行的指令都是重排后的

基于这样的情况,就会产生一个内存可见性的问题,原本单核情况下,缓存与主存具有一致性,但是多核情况下,A核对主存的修改,不会也无法去与B核中的缓存进行同步。问题的根源就在于内存同步上,多核只是引发这类问题的一个因素,类似的还有DMA区域,CPU与外设驱动都拥有DMA区域的写入权限,就会带来竞争。

Java和C语言中,有关键字Voliate,用于标记变量直读主存,避免内存的不可见性带来的影响。可见,在享受缓存带来的便利的时候,仍然需要付出两个代价:

  • 缓存未命中时,还是需要访问主存
  • 多处理器情况下,需要通过某种方式实现共享内存的一致性协议

可以想到,如果能够规范代码执行过程,将某个变量或者某段代码的所有权限定一个时间内只能有一个线程,不会被打断,那么自然不会有竞争,这样的操作就是原子性的(atomic),或者我们限定编译器与CPU对某个变量的读写与内存访问行为,把存在互相依赖关系的内存操作以特定的顺序执行,禁止CPU和编译器的乱序,内存屏障就是这样的一种干预手段,它们会给屏障

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值