Memory barrier 简介
Memory barrier 简介
程序在运行时内存实际的访问顺序和程序代码编写的访问顺序不一定一致,这就是内存乱序访问。内存乱序访问行为出现的理由是为了提升程序运行时的性能。内存乱序访问主要发生在两个阶段:
- 编译时,编译器优化导致内存乱序访问(指令重排)
- 运行时,多 CPU 间交互引起内存乱序访问
Memory barrier 能够让 CPU 或编译器在内存访问上有序。一个 Memory barrier 之前的内存访问操作必定先于其之后的完成。Memory barrier 包括两类:
- 编译器 barrier
- CPU Memory barrier
很多时候,编译器和 CPU 引起内存乱序访问不会带来什么问题,但一些特殊情况下,程序逻辑的正确性依赖于内存访问顺序,这时候内存乱序访问会带来逻辑上的错误,例如:
// thread 1
while (!ok);
do(x);
// thread 2
x = 42;
ok = 1;
此段代码中,ok 初始化为 0,线程 1 等待 ok 被设置为 1 后执行 do 函数。假如说,线程 2 对内存的写操作乱序执行,也就是 x 赋值后于 ok 赋值完成,那么 do 函数接受的实参就很可能出乎程序员的意料,不为 42。
编译时内存乱序访问
在编译时,编译器对代码做出优化时可能改变实际执行指令的顺序(例如 gcc 下 O2 或 O3 都会改变实际执行指令的顺序):
// test.cpp
int x, y, r;
void f()
{
x = r;
y = 1;
}
编译器优化的结果可能导致y = 1
在 x = r
之前执行完成。首先直接编译此源文件:
g++ -S test.cpp
得到相关的汇编代码如下:
movl r(%rip), %eax
movl %eax, x(%rip)
movl $1, y(%rip)
这里我们看到,x = r 和 y = 1 并没有乱序。现使用优化选项 O2(或 O3)编译上面的代码(g++ -O2 -S test.cpp),生成汇编代码如下:
movl r(%rip), %eax
movl $1, y(%rip)
movl %eax, x(%rip)
我们可以清楚的看到经过编译器优化之后 movl $1, y(%rip) 先于 movl %eax, x(%rip) 执行。避免编译时内存乱序访问的办法就是使用编译器 barrier(又叫优化 barrier)。Linux 内核提供函数 barrier() 用于让编译器保证其之前的内存访问先于其之后的完成。内核实现 barrier() 如下(X86-64 架构):
#define barrier() __asm__ __volatile__("" ::: "memory")
现在把此编译器 barrier 加入代码中:
int x, y, r;
void f()
{
x = r;
__asm__ __volatile__("" ::: "memory");
y = 1;
}
这样就避免了编译器优化带来的内存乱序访问的问题了(如果有兴趣可以再看看编译之后的汇编代码)。本例中,我们还可以使用 volatile 这个关键字来避免编译时内存乱序访问(而无法避免后面要说的运行时内存乱序访问)。volatile 关键字能够让相关的变量之间在内存访问上避免乱序,这里可以修改 x 和 y 的定义来解决问题:
volatile int x, y;
int r;
void f()
{
x = r;
y = 1;
}
现加上了 volatile 关键字,这使得 x 相对于 y、y 相对于 x 在内存访问上有序。在 Linux 内核中,提供了一个宏 ACCESS_ONCE 来避免编译器对于连续的 ACCESS_ONCE 实例进行指令重排。其实 ACCESS_ONCE 实现源码如下:
#define ACCESS_ONCE(x) (*(volatile typeof(x) *)&(x))
此代码只是将变量 x 转换为 volatile 的而已。现在我们就有了第三个修改方案:
int x, y, r;
void f()
{
ACCESS_ONCE(x) = r;
ACCESS_ONCE(y) = 1;
}
到此基本上就阐述完了我们的编译时内存乱序访问的问题。下面开始介绍运行时内存乱序访问。
运行时内存乱序访问
在运行时,CPU 虽然会乱序执行指令,但是在单个 CPU 的上,硬件能够保证程序执行时所有的内存访问操作看起来像是按程序代码编写的顺序执行的,这时候 Memory barrier 没有必要使用(不考虑编译器优化的情况下)。这里我们了解一下 CPU 乱序执行的行为。在乱序执行时,一个处理器真正执行指令的顺序由可用的输入数据决定,而非程序员编写的顺序。
早期的处理器为有序处理器(In-order processors),有序处理器处理指令通常有以下几步:
- 指令获取
- 如果指令的输入操作对象(input operands)可用(例如已经在寄存器中了),则将此指令分发到适当的功能单元中。如果一个或者多个操作对象不可用(通常是由于需要从内存中获取),则处理器会等待直到它们可用
- 指令被适当的功能单元执行
- 功能单元将结果写回寄存器堆(Register file,一个 CPU 中的一组寄存器)
相比之下,乱序处理器(Out-of-order processors)处理指令通常有以下几步:
- 指令获取
- 指令被分发到指令队列
- 指令在指令队列中等待,直到输入操作对象可用(一旦输入操作对象可用,指令就可以离开队列,即便更早的指令未被执行)
- 指令被分配到适当的功能单元并执行
- 执行结果被放入队列(而不立即写入寄存器堆)
- 只有所有更早请求执行的指令的执行结果被写入寄存器堆后,指令执行的结果才被写入寄存器堆(执行结果重排序,让执行看起来是有序的)
从上面的执行过程可以看出,乱序执行相比有序执行能够避免等待不可用的操作对象(有序执行的第二步)从而提高了效率。现代的机器上,处理器运行的速度比内存快很多,有序处理器花在等待可用数据的时间里已经可以处理大量指令了。
现在思考一下乱序处理器处理指令的过程,我们能得到几个结论:
- 对于单个 CPU 指令获取是有序的(通过队列实现)
- 对于单个 CPU 指令执行结果也是有序返回寄存器堆的(通过队列实现)
由此可知,在单 CPU 上,不考虑编译器优化导致乱序的前提下,多线程执行不存在内存乱序访问的问题。我们从内核源码也可以得到类似的结论(代码不完全的摘录):
Memory barrier 简介的更多相关文章
- 内存屏障(Memory barrier)-- 转发
本文例子均在 Linux(g++)下验证通过,CPU 为 X86-64 处理器架构.所有罗列的 Linux 内核代码也均在(或只在)X86-64 下有效. 本文首先通过范例(以及内核代码)来解释 Me ...
- 并行计算之Memory barrier(内存
本文转载自:http://name5566.com/4535.html 参考文献列表:http://en.wikipedia.org/wiki/Memory_barrierhttp://en.wiki ...
- 理解 Memory barrier
理解 Memory barrier(内存屏障) 发布于 2014 年 04 月 21 日2014 年 05 月 15 日 作者 name5566 参考文献列表:http://en.wikipedia. ...
- 理解 Memory barrier(内存屏障)无锁环形队列
原文:https://www.cnblogs.com/my_life/articles/5220172.html Memory barrier 简介 程序在运行时内存实际的访问顺序和程序代码编写的访问 ...
- 理解 Memory barrier(内存屏障)【转】
转自:http://name5566.com/4535.html 参考文献列表:http://en.wikipedia.org/wiki/Memory_barrierhttp://en.wikiped ...
- memory barrier 内存屏障 编译器导致的乱序
小结: 1. 很多时候,编译器和 CPU 引起内存乱序访问不会带来什么问题,但一些特殊情况下,程序逻辑的正确性依赖于内存访问顺序,这时候内存乱序访问会带来逻辑上的错误, 2. https://gith ...
- 内存屏障 & Memory barrier
Memory Barrier http://www.wowotech.net/kernel_synchronization/memory-barrier.html 这里面讲了Memory Barrie ...
- Linux内核同步机制之(三):memory barrier【转】
转自:http://www.wowotech.net/kernel_synchronization/memory-barrier.html 一.前言 我记得以前上学的时候大家经常说的一个词汇叫做所见即 ...
- Why Memory Barrier?
引言:xchg做了什么? 首先,xchg eax, ecx并不会比mov edx, eax + mov eax, ecx + mov ecx, edx这三条指令加一起快,原因是xchg有副作用. Mi ...
随机推荐
- 启动elasticsearch报错
could not find java; set JAVA_HOME or ensure java is in PATH 首先需要安装java 1.yum list installed |grep j ...
- Linux编程 7 (实时监测进程 top, 结束进程kill,killall)
一. 实时监测进程 top 在一篇里讲到ps命令在收集进程信息时非常有用,但它只能显示某个特定时间点的信息.想要观察那些频繁换进换出的内存进程趋势,用top命令是合适的.使用top命令如下图所示: 在 ...
- CSRF理解和实战
目录 啥是CSRF攻击 写一个CSRF攻击 如何避免CSRF攻击 啥是CSRF攻击 CSRF(Cross-site request forgery)跨站请求伪造,CSRF通过伪装来自受信任用户的请求来 ...
- [TensorFlow] Creating Custom Estimators in TensorFlow
Welcome to Part 3 of a blog series that introduces TensorFlow Datasets and Estimators. Part 1 focuse ...
- .Net Core缓存组件(MemoryCache)源码解析
一.介绍 由于CPU从内存中读取数据的速度比从磁盘读取快几个数量级,并且存在内存中,减小了数据库访问的压力,所以缓存几乎每个项目都会用到.一般常用的有MemoryCache.Redis.MemoryC ...
- FFmpeg封装格式处理4-转封装例程
本文为作者原创,转载请注明出处:https://www.cnblogs.com/leisure_chn/p/10506662.html FFmpeg封装格式处理相关内容分为如下几篇文章: [1]. F ...
- Spring之Bean的注入
Bean的配置中介绍的是Bean声明问题,在哪声明怎么声明的问题.Bean的注入是怎么实例化,怎么注入的问题.Bean注入的方式有两种,一种是在XML中配置,另一种则是使用注解的方式注入. 一.XML ...
- S3C2440 DMA 驱动示例
将 DMA 抽象为一个字符设备,在初始化函数中调用 void *dma_alloc_writecombine(struct device *dev, size_t size, dma_addr_t * ...
- [转]【Angular4】基础(二):创建组件 Component
本文转自:https://blog.csdn.net/u013451157/article/details/79445138 版权声明:本文为博主原创文章,未经博主允许不得转载. https://bl ...
- .net core Identity集成IdentityServer(2) 实现IprofileService接口在accesstoken中增加自定义claims
导读 1. 如何添加自定义的claims. 前请提要 目前我们拥有了三个web应用. localhost:40010, 验证服务器 localhost:40011, mvc客户端, 充当webapp请 ...