棋子 · 2023年01月04日 · 北京市

ARM系列 -- 内存屏障

在开始学习ARM内存屏障(memory barrier)指令前,需要想了解几个相关的概念:内存模型(memory model),内存类型(memory type),内存属性(memory attribute)。

关于这几个概念,前面的文章讲过。为了保持本篇的内容完整性,今天再重复一遍。

第一个是内存模型,有的翻译成存储模型。Armv8-A采用的是弱序内存模型(weakly ordered model of memory),也就是说实际的内存访问顺序可能与程序的load/strore操作顺序不完全一致。为什么实际访问顺序与程序顺序不一致呢?或者换一种问法,能不能让二者的顺序一致呢?答案是可以,但是处理器会有性能损失。如果让两者的顺序完全一致,就是强序内存模型,也称为顺序一致性模型。在此模型下,load/store操作是顺序的访问存储器。处理器都按照程序顺序来执行程序,即便访问的是不同内存地址,也不能改变访问顺序。从全局看,每个内存写操作都需要能被系统中所有的处理器同时观测到,同一时刻只有一个处理器和内存系统相连,因此对内存的访问是原子化的,串行化的。但这里有一个问题,处理器只有在一个操作完成后才能执行下一个操作。我们知道,现代高性能处理器的主频很高,芯片的内存速度远远落后于处理器速度,如果处理器发起下一个访存操作前必须等在上一个访存操作完成,那么必然造成处理器停顿(CPU stall),处理器的性能将会被大大影响。

弱序内存模型对于访存顺序的要求不像顺序模型那么严格,只要同步访问之间的次序得到保证,load和store指令的执行就可以改变次序或相互叠加,无需原子性的执行这些指令。换句话说,处理器可以按照一定规则对访存指令进行重新排序。

目前,高性能处理器可以支持推测性内存读取(speculative memory read)、指令多发射(multiple issuing of instruction),乱序执行(out-of-order execution)等多种技术。这些技术与其它技术一起,为访存的硬件重新排序提供了进一步的可能性:

  • 指令多发射:处理器可以在每个时钟周期发出和执行多条指令。一些指令可以并行到达流水线的执行阶段,因此这些指令的执行顺序可能会以与程序中的顺序不同。
  • 乱序执行:此技术允许处理器可以乱序的执行非相关(依赖)的指令,一些指令可能因为某些原因暂时停留在执行阶段,但这些指令不会阻止其它的非相关指令完成。
  • 推测:处理器在执行条件指令(例如分支指令)时,可以根据一定的规则进行推测,尽可能早的装入指令,也就是尽量填充流水线,这样处理器就不会空闲着。
  • load/store优化:处理器为了减少访存次数,可以把多个访存操作合并成一笔操作。
  • 编译器优化:优化编译器可以对指令重新排序,以隐藏延迟或充分利用硬件功能。在单核系统中,这种重新排序的影响对程序员来说是透明的,因为单个处理器可以检查并确保指令的依赖性,避免竞争现象。但是在多核系统中,处理器核之间共享存储,共享数据,目前编译器没有办法知道处理器核之间的依赖关系。

综上,内存一致性模型用于定义系统中对内存访问需要遵守的原则,在高性能处理器设计需要慎重考虑。内存一致性问题不同于缓存一致性,虽然缓存一致性会加重内存一致性的难度。换句话说,即使关闭高性能处理器中的缓存,内存一致性问题依然存在。

第二个概念,内存类型。Armv8-A架构定义了两种互斥的内存类型,普通型(normal)和设备型(device),所有的内存区域都配置为这两种类型中的一种。

1. 普通型内存:

普通型内存主要包括RAM,ROM,Flash等。普通型内存是弱序的,允许编译器进行更多的优化,以支持高性能处理器。处理器可以推测性的访问标记为普通型的存储地址,以便可以从中读取数据或指令,而无需在程序中显式引用。为了获得最佳性能,需要将应用程序的代码和数据标记放在普通型内存中。如果需要严格的内存访问顺序,即在需要强制排序的情况下,可以通过使用显式屏障操作来实现。

处理器必须始终负责由地址依赖性引起的危险:

STR X0, [X2]

LDR X1, [X2]

这个例子中,第一条指令的意思是把X0寄存器中的值写入(store)到以X2寄存器的值为地址的存储位置;第二条指令是把X2寄存器的值为地址的数据加载(load)到X1处理器中,两条指令执行完的效果是把X0的值写到外部存储(地址保存在X2中),并且把这个值赋给X1。由于这两条指令都以X2寄存器的值为地址,因此便产生了地址依赖关系。如果把两条指令重新排序,先执行load再执行store,显然得到的结果不是预期的。

2. 设备型内存:

设备型内存主要指的是memory-mapped的外设空间。对设备型内存的访问,其要求比普通型严格得多。ARM架构中禁止对设备型内存进行任何推测性读取(speculative read)操作,也不建议在标记为设备型的存储空间执行程序,因为其结果是不可预测的。

根据访存需遵守的规则,设备型内存分为以下四类:

  • Device-nGnRnE
  • Device-nGnRE
  • Device-nGRE
  • Device-GRE

image.png

这里不再啰嗦,想具体了解的话去翻前面的两篇文章。

第三个概念,内存属性。系统的整个存储空间往往被分成分为几个区域,每个区域可以设置不同的属性,例如允许访问的特权等级属性,存储类型属性,可缓存(cacheability)属性,可共享(shareability)属性,缓存策略等等。

image.png

ARM中引入了一个“共享域(shareable domain)”的概念,主要用于指定屏障指令和缓存维护指令的作用范围,为的是减小带宽等开销。
image.png

  • Non-shareable:不需要与其它内核、处理器或设备同步的访问;该域通常不用于SMP系统。
  • Inner Shareable:由几个代理共享的域,但不一定是系统中的所有代理;一个系统可以有几个内部共享域;影响一个内部可共享域的操作不会影响系统中的其他内部可共享域。
  • Outer Shareable:可以由一个或多个内部可共享域组成;影响外部可共享域的操作也会影响其中的所有内部可共享域。
  • Full system:整个系统上的操作会影响系统中的所有观察者。

单看解释比较晦涩,以上图为例,最左侧的处理器核不需要与其它三个处理器核共享数据,因此划分到不可共享域;其它三个处理器共享数据且需要同步,构成内部可共享域;这三个处理器核还要与GPU共享数据,构成一个外部可共享域。

我们来看一下Armv8-A中关于内存页表的描述,先看最低的3-bit,AttrIndx[2:0]。
image.png
内存属性没有直接放在页表项中,而是放在了寄存器MAIR_ELx中。其中的MAIR是Memory Attribute Indirection Register的缩写,EL是Exception Level,x是异常等级编号,从0-3。MAIR_ELx.Attrn定义了内存类型。MAIR_ELx的attr共分成8段,可以通过上图中的AttrIndx[2:0]来索引。
image.png
内存页表描述中的SH[1:0]即为共享域的定义,编码如下图。
image.png
终于铺垫完了,可以开始今天的正题了。在某些时候,指令重新排序会导致程序运行与预期不符,因此需要在程序中显式的规定一些指令的执行顺序。这时就用到了屏障指令。

Armv8-A提供了几种内存屏障指令:

ISB(Instruction Synchronization Barrier)指令可以确保程序中在ISB指令后的所有指令,只有在ISB指令执行完才会从缓存或者内存中取出。ISB用于确保任何先前执行的上下文更改操作(如写入系统控制寄存器)在ISB指令完成前已完成,且对ISB指令之后的指令可见。其架构文档中给出一个需要插入ISB指令的例子:处理器执行缓存和TLB维护指令;执行ISB指令;等待前面的指令完成后更改系统寄存器。

DMB(Data Memory Barrier)指令是一种内存屏障指令,它确保了屏障之前的内存访问与之后的内存访问的相对执行顺序。DMB指令不能确保内存访问的完成顺序。文档中给出了一个例子及解释,注意其中ADD指令,因为这不是内存访问指令,因此可以重排序到DMB之前执行。

image.png

DSB(Data Synchronization Barrier)指令强制执行与DMB相同的顺序,但它也会阻止任何其他指令的执行,而不仅仅是load和store,直到同步完成。所以,DSB要比DMB严格。在下面的例子中,ADD指令就不可以重排序到DSB指令之前执行。

image.png

DMB和DSB指令需要带一个参数,该参数指定了屏障指令的访问类型和应用的共享域范围,如下表。参数的前缀字母表示DSB或DMB的作用域,比如OSH表示outer shareable,ISH表示inner shareable,后缀的两个字母表示DSB或DMB的访问方向,比如LD表示load,其含义是屏障指令后面的load和store指令必须要等前面的load指令完成;ST表示store,其含义是屏障指令后面的store指令必须要等前面的store指令执行完,但是屏障指令后面的load指令不受约束;如果没有后缀,则表示屏障指令之后的任何load/store指令必须要等屏障指令之前的load/store指令完成。

image.png
表中的Load - Load/Store表示在屏障之前完成所有load,但不需要完成store,程序顺序中出现在屏障之后的load和store都必须等待屏障完成。;Store – Store表示屏障仅影响store访问,load仍然可以绕过屏障自由重新排序;Any – Any表示load和store必须在屏障之前完成,程序顺序中出现在屏障之后的load和store都必须等待屏障完成。

除了上述的ISB,DMB和DSB,还有几个不常见的屏障指令。

SB(Speculation Barrier)指令阻止指令的推测性执行,直到屏障完成后。在SB完成之前,程序顺序中出现的任何指令的推测性执行都晚于SB指令。

CSDB(Consumption of Speculative Data Barrier)指令用于控制推测执行和数据值预测,包括:任何指令的数据值预测;任何指令的PSTATE.{N,Z,C,V}预测;SVE预测。

SSBB(Speculative Store Bypass Barrier)指令在某些情况下,可以防止推测性加载绕过早期存储到同一虚拟地址。

PSB CSYNC(Profiling Synchronization Barrier)它确保当前PE的所有现有分析数据(profiling data)都已格式化,并且分析缓冲区地址已转换,以便启动对分析缓冲区的所有写入操作。

PSSBB (Physical Speculative Store Bypass Barrier)指令在某些情况下,可以防止推测性加载绕过早期存储,到达相同的物理地址。

TSB CSYNC (Trace Synchronization Barrier)指令保留了对系统寄存器的内存访问的相对顺序。

这几条屏障指令在Armv8-A架构文档中的描述不多,感兴趣的可以再去查查其它文档,看看是否有相关的描述。

Armv8-A还提供了一组Load-Acquire(LDAR)和Store-Release(STLR)指令,可以用于支持释放一致性(Release Consistency)模型。释放一致性模型是对弱序一致性模型的改进,它把同步操作进一步分成获取(acquire)操作和释放(release)操作。

image.png

根据上述,可以看出来LDAR和STLR是单向的屏障。LDAR指令仅保证之后的任何内存访问指令在LDAR后可见,不限制LDAR之前的内存访问指令向后重排序。STLR仅保证在所有早期的内存访问在STLR之前都是可见的,不限制后面的内存访问指令向前重排序。所以LDAR和STLR的限制比DMB和DSB要宽松。这一段的英文描述比较绕,转成汉语更不好表达,有点像绕口令了,还是参考下图吧。

image.png

上图中的LDAR和STLR组成了一个临界区。灰色区域的load/store指令不能向前或向后重排序,但是LDAR上面的load/store指令可以先后重排序,STLR下面的load/store指令可以向前重排序。

总结一下,由于多种原因,高性能处理器支持指令重新排序。如果内存访问指令间有数据或者地址依赖关系,指令重排序不会打乱这种关系,如果没有依赖关系,内存访问指令就可能会被重新排序,因此内存访问顺序与程序预期顺序可能不符。对于并行程序而言,可能就会导致错误。通过内存屏障指令,可以强制内存访问指令的执行顺序,虽然会对性能造成影响,但是可以保证某些内存访问顺序符合程序预期。

今天就到这里吧,是不是没用的知识又增加了一些呢?

作者: 老秦谈芯
文章来源: 老秦谈芯

推荐阅读

更多IC设计技术干货请关注IC设计技术专栏。
迎添加极术小姐姐微信(id:aijishu20)加入技术交流群,请备注研究方向。
推荐阅读
关注数
19122
内容数
1295
主要交流IC以及SoC设计流程相关的技术和知识
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息