内存屏障 – MemoryBarrier[通俗易懂]

内存屏障 – MemoryBarrier[通俗易懂]处理器的乱序和并发执行目前的高级处理器,为了提高内部逻辑元件的利用率以提高运行速度,通常会采用多指令发射、乱序执行等各种措施。现在普遍使用的一些超标量处理器通常能够在一个指令周期内并发执行多条指令。处理器从L1I-Cache预取了一批指令后,就会分析找出那些互相没有关联可以并发执行的指令,然后送到几个独立的执行单元进行并发执行。比如下面这样的代码(假定编译器不做优化):  z

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

处理器的乱序和并发执行

目前的高级处理器,为了提高内部逻辑元件的利用率以提高运行速度,通常会采用多指令发射、乱序执行等各种措施。现在普遍使用的一些超标量处理器通常能够在一个指令周期内并发执行多条指令。处理器从L1 I-Cache预取了一批指令后,就会分析找出那些互相没有关联可以并发执行的指令,然后送到几个独立的执行单元进行并发执行。比如下面这样的代码(假定编译器不做优化):

    z = x + y;
    p = m + n; 
CPU就有可能将这两行无关代码分别送到两个算术单元去同时执行。像Freescale的MPC8541这种嵌入式处理器一个指令周期能够加载4条指令、发射2条指令到流水线、用5个独立的执行单元来并发执行。

通常来说访存指令(由LSU单元执行)所需要的指令周期可能很多(可能要几十甚至上百个周期),而一般的算术指令通常在一个指令周期就搞定。所以有 可能代码中的访存指令耗费了多个周期完成执行后,其他几个执行单元可能已经把后面有多条逻辑上无关的算术指令都执行完了,这就产生了乱序。

另外访存指令之间也存在乱序的问题。高级的CPU可以根据自己Cache的组织特性,将访存指令重新排序执行。访问一些连续地址的可能会先执行,因为这时候Cache命中率高。有的还允许访存的Non-blocking,即如果前面一条访存指令因为Cache不命中,造成长延时的存储访问时,后面的访存指令可以先执行以便从Cache取数。对写指令的访存乱序有可能造成的错误后果,所以处理器通常有专门的机制(通常是做了个缓冲)保证在出现异常或者错误的时候,可以丢弃异常点后面的写指令的结果不做写入。

处理器的分支预测功能也能引起并发执行。处理器的分支预测单元有可能直接把两条分支的指令都预取来一块并发执行掉。等到分支判断的结果出来以后,再丢弃错误分支的计算结果。这样在很多情况下可以实现0周期跳转。比如这样的代码(假定编译器不做优化):

    z = x + y; 
    if (z > 0) then
    p = m + n;
    else
    p = m – n; 
看上去如果z不计算出来是无法继续的。但是实际上CPU有可能先把三个加法都同时进行计算,然后根据z=x+y的结果直接挑选正确的p值。

因此,即使是从汇编上看顺序正确的指令,其执行的顺序也是不可预知的。处理器能够保证并发和乱序执行不会得到错误结果,但是如果是对一些硬件寄存器的操作不能允许乱序的话,程序员就必须把这个情况告诉CPU。告诉的方法就是通过CPU提供的一组同步指令实现,通常在CPU的文档里面有对同步指令的使用说明。系统函数库里面的内存屏障(rmb/wmb/mb)实际上也是通过这些同步指令实现的。因此在C编码的时候,只要设置好内存屏障,就能告诉CPU 哪些代码是不能乱序的。

编译器的乱序优化

受到处理器预取单元的能力限制,处理器每次只能分析一小块指令的并发性,如果指令相隔比较远就无能为力了。但是从编译器的角度来看,编译器能够对很大一个范围的代码进行分析,能够从更大的范围内分辨出可以并发的指令,并将其尽量靠近排列让处理器更容易预取和并发执行,充分利用处理器的乱序并发功能。所以现代的高性能编译器在目标码优化上都具备对指令进行乱序优化的能力。并且可以对访存的指令进行进一步的乱序,减少逻辑上不必要的访存,以及尽量提高 Cache命中率和CPU的LSU(load/store unit)的工作效率。所以在打开编译器优化以后,看到生成的汇编码并不严格按照代码的逻辑顺序是正常的。和处理器一样,如果想要告诉编译器不要去对某些指令乱序优化,也要通过一些方式来告诉编译器。通常可以通过volatile关键字来抑制(注意,不是禁止)编译器对相关变量的访问优化。举个例子:

    int *p, *q; 
    ……; 
    *p = 1; 
    *p = 2; 
    *q = *p; 
这样,编译器通常会优化掉前面一个对*p的写入(逻辑上冗余),仅对*p写入2。而对*q赋值的时候,编译器认为此时*q的结果就应该是上次*p的值,会优化掉从*p取数的过程,直接把在寄存器中保存的*p的值给*q(PowrPC汇编):

(假设r3=p,r4=q) 
    li r5, 2 // r5赋值2 
    stw r5, 0(r3) // 把r5写到*p 
    stw r5, 0(r4) // 把r5写到*q 
但是如果为p指针加上了volatile关键字,情况就不同了:

    volatile int *p; 
    int *q; 
    ……; 
    *p = 1; 
    *p = 2; 
    *q = *p; 
在这种情况下,编译器看见*p是volatile的时候,就会:

不对*p操作生成乱序指令(通常如此,具体请看后面的解释)

每次从*p取数据的时候,一定会进行一次访存操作,哪怕前面不久才取过*p的值放在寄存器里。

不合并对*p的写操作(也只是通常如此,解释见后)

所以这回的结果如下(PowrPC汇编):

(假设r3=p,r4=q) 
    li r5, 1 // r5赋值1 
    stw r5, 0(r3) // 把r5写到*p 
    li r5, 2 // r5赋值2 
    stw r5, 0(r3) // 把r5写到*p 
    lwz r5, 0(r3) // 从*p取值到r5 
    stw r5, 0(r4) // 把r5写到*q 
这样编译器会在汇编码级别保证指令有序和不优化掉访存操作。通常简单地使用volatile关键字就可以解决编译器的乱序问题,但是这些指令到了处理器执行的时候,仍然可能被乱序。对于处理器乱序执行的避免就需要用到一组内存屏障函数(barrier)了。

重要 

绝大多数的编译器,通常不会优化掉对volatile对象的访问,并且通常保持同一个volatile对象的一系列读写操作是有序的(但是不能保证不同的volatile对象之间有序)。

但是,这不是绝对的。因为ANSI C99标准关于对volatile对象访问时编译器是否要绝对保证禁止乱序(reorder)和禁止访问合并(combine access)并没有做任何规定!仅仅是鼓励编译器最好不要去优化对volatile对象的访问,而唯一的强制要求仅仅是要求编译器保证对 volatile对象的访问优化不会跨越“sequence point”即可(所谓sequence point是指一些诸如外部函数调用、条件或循环跳转等关键点,具体定义请查阅C99标准内的详细说明)。

这就是说,如果一个编译器在两个sequence point之间像对待普通变量一样去优化volatile变量,也是完全符合C99标准的!比如:

   volatile int a;

    if (…) { … } // sequence point
    a = 1;
    a = 2;
    a = 3;
    printk(“…”); // sequence point 
在两个sequence point之间,要是有编译器对a的赋值操作合并(即仅写入3)或者乱序(如写1和写2对调),都是完全符合C99标准的。所以,我们在使用的时候,不能指望用了volatile以后绝对能生成有序的完整的汇编码,即不要指望volatile来保证访存有序。实质上 volatile最大的作用主要还是在保证每次使用从内存中取值,而并不能保证编译器不做其他任何优化(毕竟volatile从字面上看意思是“易变”而不是“有序”。编译器只保证对volatile对象即时更新但不保证访问有序也不是说不过去的)。

从另一个角度看,即使是编译器生成的汇编码有序,处理器也不一定能保证有序。就算编译器生成了有序的汇编码,到了处理器那里也拿不准是不是会按照代码顺序执行。所以就算编译器保证有序了,程序员也还是要往代码里面加内存屏障才能保证绝对访存有序,这倒不如编译器干脆不管算了,因为内存屏障本身就是一个sequence point,加入后已经能够保证编译器也有序。

因此,对于切实是需要保障访存顺序的代码,就算当前使用的编译器能够编译出有序的目标码来,我们也还是必须通过设置内存屏障的方式来保证有序,否则都是不严谨,有隐患的。

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

发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/157925.html原文链接:https://javaforall.cn

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)


相关推荐

  • 何时使用数据库存储过程

    何时使用数据库存储过程

  • servlet异步请求

    servlet异步请求1、什么是servlet异步请求Servlet3.0之前,一个普通Servlet的主要工作流程大致如下:(1)、Servlet接收到请求之后,可能需要对请求携带的数据进行一些预处理;(2)、调用业务接口的某些方法,以完成业务处理;(3)、根据处理的结果提交响应,Servlet线程结束。其中第二步处理业务逻辑时候很可以碰到比较耗时的任务,此时servlet主线程会阻塞等待完成业务处理,对于并发比较大的请求可能会产生性能瓶颈,则servlet3.0之后再此处做了调整,引入了…

  • jboss安装与配置_微信最新版下载并安装

    jboss安装与配置_微信最新版下载并安装jboss有开源和商业两个版本,他们区别如下:JBossAS开源社区版本,发布比较频繁。JBoss7,先后发布了7.0.0,7.0.1,7.0.2,7.1.0,7.1.1,7.1.2,7.1.3,7.2.0,其中7.1.1比较经典,7.2.0是JBossEAP6.1的基础,但7.1.2,7.1.3,7.2.0只是源代码打了Tag,并没提供开放下载。JBossEAP(EnterpriseApplicationPlatform)在开源版本上构建的企业版本,目

  • Springboot引入本地jar包,并通过maven把项目成功打包成jar包部署[通俗易懂]

    Springboot引入本地jar包,并通过maven把项目成功打包成jar包部署[通俗易懂]引入钉钉的sdk打包到线上,各种报错:ClassNofFoundException…第一步:将jar包放到resource的lib文件夹下:第二部:在pom文件中引入:第三部:继续修改pom文件的打包插件配置:<includeSystemScope>true</includeSystemScope>打包时候IDEA还是会报警告,不用管它;…

  • 论文写作利器—LaTeX教程(入门篇)(更新中)

    论文写作利器—LaTeX教程(入门篇)(更新中)一、LaTeX简介结合维基百科及LaTeX官网可知:LaTeX(/ˈlɑːtɛx/,常被读作/ˈlɑːtɛk/或/ˈleɪtɛk/)是一种基于TeX的高品质排版系统,由美国计算机科学家莱斯利·兰伯特在20世纪80年代初期开发,非常适用于生成高印刷质量的科技和数学、物理文档,尤其擅长于复杂表格和数学公式的排版。LaTeX是科学文献交流和出版的事实标准。简单来说,相比于Word排版时需要设…

  • html 网页不可以复制粘贴,网页上的文字不能复制,三种方法教你复制全网文字…

    html 网页不可以复制粘贴,网页上的文字不能复制,三种方法教你复制全网文字…有时候在网上找文字素材的时候,一般做法是直接复制粘贴使用。但你有没有遇到过不能复制的时候?反正小编是有的!好不容易找到了需要的文字却复制不出来,是不是好让人闹心?!有什么办法可以避开这个限制,将网页的文字复制下来为我所用呢?今天小编就分享三种方法给大家,不多不少就三种,帮你轻松搞定网页限制,复制想要的文字~至于哪种最好,还是看大家的喜好啦!1、Ctrl+P没错,就是这个快捷键。p代表print,打…

    2022年10月13日

发表回复

您的电子邮箱地址不会被公开。

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