Skip to content

内存屏障

编译器为了提高性能,在对指令优化时会对指令的顺序重新排列,这就是所谓的指令乱序。编译时的乱序访问在C语言中可以通过barrier()函数来规避:

#define barrier() __asm__ __volatile__("": : :"memory")

在多处理器系统中,一个 CPU 核心的指令乱序可能会对其他 CPU 核心产生影响,进而造成不可预期的行为,特别是多核并发访问共享资源时。由于现代处理器普遍采用诸如高速缓存、超标量技术、乱序执行、多级流水线技术等,这些技术都会对指令的执行顺序进行重新排序,因此,为了确保程序在多处理器系统上的正确性,我们就需要合理使用内存屏障指令来确保程序按照我们预期的方式执行。

ARM64 处理器的屏障指令包括:

  • DMB(数据内存屏障)指令:保证在DMB指令之前的所有内存访问操作都已完成,才提交DMB指令之后的内存访问操作。
  • DSB(数据同步屏障)指令:位于此指令之前的所有内存访问、高速缓存、分支预测和 TLB 维护指令全部完成。
  • ISB(指令同步屏障)指令:flush 流水线,使得在ISB指令之后的指令重新从指令缓存或内存中重新预取。

Linux 内核中的自旋锁、互斥体等逻辑,都使用了内存屏障指令来确保线程安全。

共享属性域

DMBDSB指令后面必须要带参数,用于指定共享属性域(share ability domain)。共享属性域用来描述内存屏障指令的作用域,ARMv8 体系结构定义了4种域:

  • 全系统共享域:影响系统中的所有观察者
  • 外部共享域:由一个或多个内部共享域组成
  • 内部共享域:由多个处理器共享的可共享域,比如一个四核 Cortex-A57 集群
  • 不指定共享域:只能由单个处理器访问

内存屏障指令的参数如下表所示:

参数 共享属性
SY 全系统共享域
OSH 外部共享域
ISH 内部共享域
NSH 不指定共享域

还可以对内存屏障指令限定读写方向:仅读——LD 后缀,仅写——ST 后缀。比如ISHLD就是仅限制内部共享域的内存读指令。

DMB指令

DMB指令只能保证前后的内存访问指令按照顺序执行,内存访问指令包括普通的加载和存储指令,也包括数据高速缓存指令。

DMB指令后面必须带参数,用来指定共享属性域。

在深入了解DMB指令之前,有必要了解下什么是数据依赖地址依赖。数据依赖指的是相邻的读写操作有数据上的依赖关系,比如从 Xn 地址读取内容到 Xm,再把 Xm 地址中的值写入到 Xy 地址中,那么这些读写指令就存在依赖关系。伪代码如下所示:

ldr xm, [xn]
str xm, [xy]

地址依赖指的是相邻的读写操作有地址上的依赖关系,比如从 Xn 地址读取内容到 Xm 地址中,再把另外一个值写入到 Xm 地址中,那么这些读写指令就存在地址依赖关系。伪代码如下所示:

ldr xm, [xn]
str xy, [xm]

如果两条指令既没有数据依赖,也没有地址依赖,那么 CPU 就可以进行指令重排以最优化性能。比如有两条指令如下:

ldr x0, [x1]
str x2, [x3]

由于没有依赖关系,从 CPU 的角度看,先执行ldr指令还是str指令,从最终结果来看没有区别。如果想要确保 CPU一定按照写的顺序来执行代码,就可以在这两条指令中间加上DMB指令:

ldr x0, x[1]
dmb ish
str x2, [x3]

再来看一个例子:

ldr x0, [x1]
dmb ish
add x2, x3, x4

尽管在ldr指令和add指令中间有一条DMB指令,但是仍然不能保证指令执行的顺序,因为DMB指令只能保证内存访问指令的执行次序,而ADD不是内存访问指令,它是有可能在ldr指令之前执行的。要解决这个问题,就得把DMB指令换成DSB指令。

DSB指令

DSB指令要比DMB指令严格得多,DSB后面的指令必须满足下面两个条件才能开始执行:

  1. DSB指令前面的所有内存访问指令必须执行完。
  2. DSB指令前面的高速缓存、分支预测、TLB 等维护指令也必须执行完。

在多核系统中,高速缓存和 TLB 维护指令会广播到其他 CPU 核心,执行本地相关的维护操作。DSB指令等待广播并收到其他 CPU 核心发送的应答信号才算执行完。

ldr x0, [x1]
dsb ish
add x2, x3, x4

当使用了DSB指令后,ADD指令不能重排到LDR指令前面。

ISB指令

ISB指令会冲刷流水线,然后从指令高速缓存后者内存中重新预取指令。

ARMv8 体系结构中有一个术语——更改上下文操作(context-switching operation),包括高速缓存、TLB、分支预测以及改变系统控制寄存器等操作。更改上下文操作的效果仅仅在上下文同步事件之后能看到。上下文同步事件分为三种:

  • 发生一个异常
  • 从一个异常返回
  • 执行了ISB指令

ISB指令刷新流水线,并从缓存或内存中重新获取指令,确保ISB指令之前的上下文更改操作对ISB指令之后的任何指令都可见。它还确保ISB指令之后的任何上下文更改操作对ISB指令之前的指令不可见。

mrs x1, cpacr_el1
orr x1, x1 # (0x3 << 20)
msr cpacr_el1, x1

isb

fadd s0, s1, s2

将 cpacr_el1的 Bit[21:20] 修改为 0x3,可以打开浮点运算功能。紧接着的FADD指令如果没有ISB指令的保护,则可能会提前执行,即浮点运算功能还未启用时就执行,这会引发错误。

案例分析请参考ARM64体系结构编程与实践P300页 ~ P311页。