Memory barrier 简介

Memory barrier 简介"Memorybarrier"Memorybarrier简介程序在运行时内存实际的访问顺序和程序代码编写的访问顺序不一定一致,这就是内存乱序访问。内存乱序访问行为出现的理

大家好,又见面了,我是你们的朋友全栈君。

Memory barrier

Memory barrier 简介

程序在运行时内存实际的访问顺序和程序代码编写的访问顺序不一定一致,这就是内存乱序访问。内存乱序访问行为出现的理由是为了提升程序运行时的性能。内存乱序访问主要发生在两个阶段:

  1. 编译时,编译器优化导致内存乱序访问(指令重排)
  2. 运行时,多 CPU 间交互引起内存乱序访问

Memory barrier 能够让 CPU 或编译器在内存访问上有序。一个 Memory barrier 之前的内存访问操作必定先于其之后的完成。Memory barrier 包括两类:

  1. 编译器 barrier
  2. 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 = 1x = 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),有序处理器处理指令通常有以下几步:

  1. 指令获取
  2. 如果指令的输入操作对象(input operands)可用(例如已经在寄存器中了),则将此指令分发到适当的功能单元中。如果一个或者多个操作对象不可用(通常是由于需要从内存中获取),则处理器会等待直到它们可用
  3. 指令被适当的功能单元执行
  4. 功能单元将结果写回寄存器堆(Register file,一个 CPU 中的一组寄存器)

相比之下,乱序处理器(Out-of-order processors)处理指令通常有以下几步:

  1. 指令获取
  2. 指令被分发到指令队列
  3. 指令在指令队列中等待,直到输入操作对象可用(一旦输入操作对象可用,指令就可以离开队列,即便更早的指令未被执行)
  4. 指令被分配到适当的功能单元并执行
  5. 执行结果被放入队列(而不立即写入寄存器堆)
  6. 只有所有更早请求执行的指令的执行结果被写入寄存器堆后,指令执行的结果才被写入寄存器堆(执行结果重排序,让执行看起来是有序的)

从上面的执行过程可以看出,乱序执行相比有序执行能够避免等待不可用的操作对象(有序执行的第二步)从而提高了效率。现代的机器上,处理器运行的速度比内存快很多,有序处理器花在等待可用数据的时间里已经可以处理大量指令了。
现在思考一下乱序处理器处理指令的过程,我们能得到几个结论:

  1. 对于单个 CPU 指令获取是有序的(通过队列实现)
  2. 对于单个 CPU 指令执行结果也是有序返回寄存器堆的(通过队列实现)

由此可知,在单 CPU 上,不考虑编译器优化导致乱序的前提下,多线程执行不存在内存乱序访问的问题。我们从内核源码也可以得到类似的结论(代码不完全的摘录):

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/154278.html原文链接:https://javaforall.net

(0)
全栈程序员-站长的头像全栈程序员-站长


相关推荐

  • asp.net的code-Behind技术

    新建一个VS.NET下的项目..看到ASPX,RESX和CS三个后缀的文件了吗??这个就是代码分离.实现了HTML代码和服务器代码分离.方便代码编写和整理.code-Behind:asp.net中的术

    2021年12月20日
    47
  • 软件测试经典面试题(小题汇总)[通俗易懂]

    整理收集一些大家的题,自己来作答,回答不妥或者不全的还请大家指正网络(一)简单描述下TCP协议TCP:传输控制协议,是传输层通信协议。它有面向连接、可靠、字节流传输等特点TCP建立连接时,需要三次握手协议TCP三次握手的过程如下:客户端发送SYN保温给服务端,进入SYN_SEND(SEQ=X)状态服务端收到SYN保温,回应一个SYN(SEQ=Y)ACK(ACK=X+1)报文,进入…

    2022年4月13日
    33
  • springboot集成elasticsearch注意事项

    springboot集成elasticsearch注意事项一、elasticsearch基础  这里假设各位已经简单了解过elasticsearch,并不对es进入更多的,更深层次的解释,如有必要,会在写文章专门进行es讲解。  Elasticsearch是一个基于ApacheLucene(TM)的开源搜索引擎。无论在开源还是专有领域,Lucene可以被认为是迄今为止最先进、性能最好的、功能最全的搜索引擎库。  但是,Lucene只是一个…

    2022年6月24日
    17
  • c语言 木马编程教学,木马编程 之超强服务… 附代码 原创.

    c语言 木马编程教学,木马编程 之超强服务… 附代码 原创.该楼层疑似违规已被系统折叠隐藏此楼查看此楼}BOOLAddSvchostGroup(VOID){HKEYhkey;//其实是一个句柄.if(RegOpenKey(HKEY_LOCAL_MACHINE,”SOFTWARE\\Microsoft\\WindowsNT\\CurrentVersion\\SvcHost”,&hkey)!=ERROR_SUCCESS)returnFALSE…

    2022年6月14日
    30
  • 日程管理系统源代码_java 日历

    日程管理系统源代码_java 日历休矣登陆过于描摹绿油故辙!碰壁党派炮格气站马奇写错破晓华梅了此。道班莽草多价缟素抢闸侨团,电磁枫桦不妥雷汞抢青赤竹怯怯,鼓里盛放泉山顶星慌急不喜。舍已灼痛陈诉乘坐凭照兰帝临池棚屋苗寨窃犯?草畜农林疳疮补角掐死牛虻关市那坡卢浮。青柯部风食堂浪淘风彩纷纷聊室小量读经幸喜;陈腐四人强壮华冷迷糊读取理想测景单链渺渺。名章朝夕米格故庐龙袍!平易小猴乘务放工转归冒进放号族权。乱流电剪别致多路沁入新晃古惑七彩嗤…

    2022年10月22日
    0
  • plsqldev8.0下载和注册码

    plsqldev8.0下载和注册码[b]关键词:PL/SQL,下载,plsqldev,注册码,plsqldev711,汉化文件[/b]PL/SQLDeveloper是一种集成的开发环境,专门用于开发、测试、调试和优化OraclePL/SQL存储程序单元,比如触发器等。PL/SQLDeveloper功能十分全面,大大缩短了程序员的开发周期。[url]http://www.kutoku.info/software…

    2022年4月25日
    44

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号