`
weiyuhu
  • 浏览: 235867 次
  • 性别: Icon_minigender_1
  • 来自: 南京
社区版块
存档分类
最新评论

内存屏障

阅读更多
处理器的乱序和并发执行

目前的高级处理器,为了提高内部逻辑元件的利用率以提高运行速度,通常会采用多指令发射、乱序执行等各种措施。现在普遍使用的一些超标量处理器通常能够在一个指令周期内并发执行多条指令。处理器从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的时候,就会:

   1.

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

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

      不合并对*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,加入后已经能够保证编译器也有序。

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

Barrier函数可以在代码中设置屏障,这个屏障可以阻挡编译器的优化,也可以阻挡处理器的优化。

对于编译器来说,设置任何一个屏障都可以保证:

   1.

      编译器的乱序优化不会跨越屏障,即屏障前后的代码不会乱序;
   2.

      在屏障后所有对变量或者地址的操作,都会重新从内存中取值(相当于刷新寄存器中的变量副本)。

而对于处理器来说,根据不同的屏障有不同的表现(以下仅仅列举3种最简单的屏障):

   1.

      读屏障rmb()
      处理器对读屏障前后的取数指令(LOAD)能保证有序,但是不一定能保证其他算术指令或者是写指令的有序。对于读指令的执行完成时间也不能保证,即它不能保证在屏障之前的读指令一定都执行完成,只能保证屏障之前的读指令一定能在屏障之后的读指令之前完成。
   2.

      写屏障wmb()
      处理器对屏障前后的写指令(STORE)能保证有序,但是不一定能保证其他算术指令或者是读指令的有序。对于写指令的执行完成时间也不能保证,即它不能保证在屏障之前的写指令一定都执行完成,只能保证屏障之前的写指令一定能在屏障之后的写指令之前完成。
   3.

      通用内存屏障mb()
      处理器保障只有屏障之前的访存操作(包括读写)都完成以后才会执行屏障之后的访存操作。即可以保障读写之间的有序(但是同样无法保证指令完成的时间)。这种屏障对处理器的执行单元效率产生的负面影响要比单纯用读屏障或者写屏障来的大。比如对于PowerPC来说这种通用屏障通常是使用sync指令实现的,在这种情况下处理器会丢弃所有预取的指令并清空流水线。所以频繁使用内存屏障会降低处理器执行单元的效率。

对于驱动开发者来说,一些对设备寄存器的操作,通常是必须保证有序的。在绝大部分情况下,一般都是写操作。对于有序的写操作,必须设置写屏障(wmb):

例:在驱动中使用写屏障

/* Mask out everything */
im_intctl->ic_simrh = 0x00000000;
im_intctl->ic_simrl = 0x00000000;

wmb(); 

/* Ack everything */
im_intctl->ic_sipnrh = 0xffffffff;
im_intctl->ic_sipnrl = 0xffffffff;

这是一个对中断控制器操作的例子。在设置两个mask寄存器的值的时候,这两个写操作没有顺序要求,因此可以不加屏障。但是对ack寄存器的设置必须在mask寄存器完成设置以后,所以在中间要加入写屏障wmb()以保证对两组寄存器的写有序。

同样的,对于一系列的只读操作,也可以简单使用rmb()来保证有序。

注意

任何一个rmb()或者wmb()都是可以被替换成mb()的。但是因为上面提到过的mb()的效率问题,所以应该只有在同时需要读屏障和写屏障的时候,才建议使用mb()。否则应该根据实际情况来选择合适的屏障。当然,在设备初始化的时候,即使是使用mb()也不会对性能带来什么影响,因为设备一般只会初始化一次。但是在发生很频繁的设备操作(比如网口的收发帧中断等)时,应该考虑到mb()对性能的影响。

如果驱动不仅仅需要在单纯的读指令或者写指令之间有序,还需要保证读写指令之间有序的时候,就需要设置mb()屏障了。下面将演示一个这样的例子:

例:使用mb()屏障保证读写有序

我们假设有一个设备,在读取设备信息时需要依次对REG1~3这三个寄存器进行写入操作(写入设备读取命令),然后才能依次读取REG4和REG5取得设备返回的信息。

REG1 = a;
wmb();  // 保证REG1和REG2的写有序

REG2 = b;
wmb();  // 保证REG2和REG3的写有序

REG3 = c;

mb();   // 保证在对设备读之前,前面的配置操作都完成(读写之间有序)

*d = REG4;
rmb();  // 保证REG4和REG5的读有序

*e = REG5;

mb();   // 保证与未来对设备的操作有序
return;

    *

      对于REG1~3的写入,可以通过设置写屏障来保证有序;
    *

      在进行REG4和5的读取之前,因为得保证前面的寄存器写操作都执行完才能读,所以需要设置一个内存屏障mb()来保证前面对寄存器的写都完成,以保障读写指令之间的有序;
    *

      后面两个读操作之间就可以通过设置读屏障来保证有序了;
    *

      最后通常在从设备操作函数返回之前,我们一般需要保证对设备的操作都执行完毕了。这样下次对设备进行操作的时候我们可以保证设备已经完成了上次操作,避免反复调用设备操作函数带来的函数间的乱序问题。所以在最后设置一个内存屏障mb(),保障和未来对设备的其他访问有序。

进一步阅读
分享到:
评论

相关推荐

    内存屏障原理解析

    内存屏障是一种在计算机科学中用来控制指令执行顺序和内存读写的同步机制。由于现代计算机系统的CPU处理速度远超过内存访问速度,为了提高性能,CPU会采用缓存(Cache)技术,而这种技术往往会导致内存操作的重新...

    内存屏障机制

    ### 内存屏障机制及其在Linux Kernel中的应用 #### 一、内存屏障基本概念 内存屏障(Memory Barrier),又称内存栅栏或内存围栏,是一种用于控制处理器内部内存操作顺序的机制。它确保某些类型的内存操作按指定...

    内存屏障访问顺序

    ### 内存屏障访问顺序详解 #### 引言 随着技术的发展与摩尔定律的推进,处理器的速度不断提升,而内存访问速度却未能跟上这一步伐。这种差异导致内存操作成为现代处理器性能瓶颈之一。为了缓解这个问题,现代...

    内核同步机制-优化屏障和内存屏障

    优化屏障和内存屏障是内核同步的两种重要手段,用于防止编译器和处理器的优化导致的数据乱序问题。 优化屏障主要用于阻止编译器对源代码指令的重排序。在Linux内核中,`barrier()`宏就是一个典型的优化屏障实现,它...

    LINUX内核内存屏障

    ### Linux内核内存屏障知识点详解 #### 一、引言 在现代计算机系统尤其是多处理器系统(SMP)中,为了提高性能,处理器通常会采用缓存机制来减少访问主存的时间延迟。然而,这种机制可能导致不同处理器之间数据的...

    linux 内核内存屏障

    ### Linux内核内存屏障知识点详解 #### 一、内存访问抽象模型 在现代计算机系统中,内存访问操作可能会出现乱序执行的现象。这种现象主要来源于CPU的指令流水线技术,该技术通过并行处理指令的不同阶段来提高...

    java内存屏障与JVM并发详解实用.pdf

    Java内存屏障与JVM并发详解实用 Java内存屏障是java并发编程中的一种机制,用于确保多线程程序的正确执行。它通过强制处理器顺序执行内存操作,从而避免了内存屏障带来的问题。在本文中,我们将深入探讨Java内存...

    内存屏障浅析

    内存屏障浅析,多线程编程,由于编译器的优化和缓存的使用,导致对内存的写入操作不能及时的反应出来,也就是说当完成对内存的写入操作之后,读取出来的可能是旧的内容

    中国Linux内核开发者大会十周年演讲稿(中兴通讯谢宝友)-Linux内存屏障

    在大会十周年的庆典上,中兴通讯的谢宝友带来了关于“Linux内存屏障”的主题演讲,这是一个深入讨论并发编程和多处理器系统中的关键技术的话题。 内存屏障,也称为内存栅栏或内存顺序约束,是CPU架构和操作系统中的...

    Linux内存屏障1

    Linux内存屏障是并行编程中一个至关重要的概念,它涉及到多核CPU的缓存一致性问题。在现代计算机系统中,由于CPU的运算速度远超内存访问速度,CPU通常会使用缓存来加速数据读写。然而,当多个CPU核心同时访问同一块...

    JVM内存模型-重排序&内存屏障 1

    在这个模型中,内存屏障(Memory Barrier)和重排序(Reordering)是两个关键概念,它们对并发编程的正确性和性能有着重要影响。 **重排序** 重排序是指编译器和处理器为了优化程序性能,可能会改变程序执行顺序的...

    乱序执行和内存屏障.pdf

    乱序执行和内存屏障 乱序执行是高级处理器中的一种技术,为了提高内部逻辑元件的利用率以提高运行速度,处理器通常会采用多指令发射、乱序执行等各种措施。乱序执行可以使处理器在一个指令周期内并发执行多条指令,...

    JVM内存模型-内存结构-内存屏障

    主要为大家讲解JVM内存模型|内存结构|内存屏障,他们的概念,有什么关联以及各种的功能

    Linux内存屏障

    Linux内存屏障是并行编程领域的重要概念,它关注的是如何在多处理器系统中保持内存操作的顺序性和一致性。在进行并行编程时,尤其是在多核CPU环境下,内存访问顺序和一致性问题尤为突出。为了解决这些问题,处理器...

    Memory Barriers: a Hardware View for Software Hackers 讲解内存屏障的好论文,推荐!

    内存屏障是一种在多处理器系统中被广泛使用的同步机制,它确保了内存操作的顺序性,对于保证多核处理器环境下软件的正确运行至关重要。为了更好地理解内存屏障的作用和它在硬件层面的表现,我们有必要先了解CPU缓存...

    Java多线程之volatile关键字及内存屏障实例解析

    内存屏障的概念内存屏障,也称为内存栅栏,是CPU或编译器用来限制特定操作的指令,它可以保证特定操作的顺序,以及保证某些数据的可见性。在Java中,volatile关键字的实现就依赖于内存屏障。内存屏障分为写屏障和读...

    wx-chevalier#Concurrent-Series#内存屏障1

    写屏障:强制将写缓冲器中的内容写入到高速缓存中,或者将屏障之后的指令全部写到写缓冲器直到之前写缓冲器中的内容全部被刷回缓存中,也就是处理 0 必须等到所有的 i

    深入硬件心脏:汇编语言中的指令锁定与内存屏障解密

    2. **低级语言**:汇编语言直接操作硬件,包括内存地址、寄存器和输入/输出端口。 3. **可读性**:虽然比机器码更易于理解,但仍然需要特定的知识和技能来编写和维护。 4. **效率**:由于直接控制硬件,汇编语言编写...

    memory-barriers.pdf

    内存屏障是计算机科学中的一个概念,主要用于多核处理器或多处理器环境中,确保数据的可见性和一致性。在Linux内核中,内存屏障是用来控制指令执行顺序的机制,以保证不同处理器或不同硬件间的一致性操作。内存屏障...

    内存相关论文

    ### 内存屏障:硬件视角下的软件黑客技术 本文探讨了内存屏障在现代多处理器系统中的重要性及其背后的原理。作者保罗·麦肯尼(Paul E. McKenney)是IBM Beaverton Linux Technology Center的一名专家,他深入剖析...

Global site tag (gtag.js) - Google Analytics