ARMv8.1指令集相对于ARMv8指令集添加了不少新的功能,其中有很大的一块功能称作LSE(Large System Extensions),这其中添加了很多平台原生就支持的原子操作指令。

在这之前,如果想实现某个原子操作,必须要使用LL/SC操作,在ARMv8以前的32位系统中使用LDREX和STREX指令,从ARMv8起,它们被改名成了LDXR和STXR。

LL/SC操作本质上是很多CPU核去抢某个内存变量的独占访问,以前ARM主要用来在低功耗设备上运行,CPU核也不会太多,不会存在太大的问题。但是,现在ARM已经往数据中心发展了,几十核的ARM处理器都已经出现了,如果还是大家一起抢可能会存在严重的性能问题。因此,为了支持这种大型系统,在ARMv8.1中特意加入了大量原生原子操作指令。

加原子操作

LDADD <Ws>, <Wt>, [<Xn|SP>]
LDADD <Xs>, <Xt>, [<Xn|SP>]

STADD <Ws>, [<Xn|SP>]
STADD <Xs>, [<Xn|SP>]

LDADD指令从第三个参数,也就是Xn或SP寄存器指定的内存位置读出32位或64位的值,将其存放进第二个参数,也就是Wt或Xt寄存器中,然后再将这个读出的值和第一个参数,也就是Ws或Xs寄存器中的值相加,再存入第三个参数指定的内存中(*(Xn|SP) += Xs),并且保证这些步骤都是原子的。

STADD指令和LDADD指令基本功能相同,只不过没有第二个参数,也就是Wt或Xt寄存器,不会返回指定内存位置上没修改之前的值。

所以,ST打头的指令和LD打头的指令,基本功能上没有什么区别,只不过LD打头的指令会把在执行该原子指令之前内存中的值存入第二个参数指定的寄存器中,而ST打头的指令没有这个功能。因此,后面只介绍LD打头指令的功能。

置位原子操作

LDSET <Ws>, <Wt>, [<Xn|SP>]
LDSET <Xs>, <Xt>, [<Xn|SP>]

STSET <Ws>, [<Xn|SP>]
STSET <Xs>, [<Xn|SP>]

LDSET指令从第三个参数,也就是Xn或SP寄存器指定的内存位置读出32位或64位的值,将其存放进第二个参数,也就是Wt或Xt寄存器中,然后再将这个读出的值和第一个参数,也就是Ws或Xs寄存器中的值进行位的或操作,再存入第三个参数指定的内存中(*(Xn|SP) |= Xs),并且保证这些步骤都是原子的。

清除位原子操作

LDCLR <Ws>, <Wt>, [<Xn|SP>]
LDCLR <Xs>, <Xt>, [<Xn|SP>]

STCLR <Ws>, [<Xn|SP>]
STCLR <Xs>, [<Xn|SP>]

LDCLR指令从第三个参数,也就是Xn或SP寄存器指定的内存位置读出32位或64位的值,将其存放进第二个参数,也就是Wt或Xt寄存器中,然后再将这个读出的值和第一个参数,也就是Ws或Xs寄存器中的值取反之后进行位的与操作,再存入第三个参数指定的内存中(*(Xn|SP) &= (NOT Xs)),并且保证这些步骤都是原子的。

异或原子操作

LDEOR <Ws>, <Wt>, [<Xn|SP>]
LDEOR <Xs>, <Xt>, [<Xn|SP>]

STEOR <Ws>, [<Xn|SP>]
STEOR <Xs>, [<Xn|SP>]

LDEOR指令从第三个参数,也就是Xn或SP寄存器指定的内存位置读出32位或64位的值,将其存放进第二个参数,也就是Wt或Xt寄存器中,然后再将这个读出的值和第一个参数,也就是Ws或Xs寄存器中的值进行位的异或操作,再存入第三个参数指定的内存中(*(Xn|SP) ^= Xs),并且保证这些步骤都是原子的。

比较存储原子操作

LDSMAX <Ws>, <Wt>, [<Xn|SP>]
LDSMAX <Xs>, <Xt>, [<Xn|SP>]

LDUMAX <Ws>, <Wt>, [<Xn|SP>]
LDUMAX <Xs>, <Xt>, [<Xn|SP>]

STSMAX <Ws>, [<Xn|SP>]
STSMAX <Xs>, [<Xn|SP>]

STUMAX <Ws>, [<Xn|SP>]
STUMAX <Xs>, [<Xn|SP>]

LDSMAX指令从第三个参数,也就是Xn或SP寄存器指定的内存位置读出32位或64位的值,将其存放进第二个参数,也就是Wt或Xt寄存器中,然后再将这个读出的值和第一个参数,也就是Ws或Xs寄存器中的值比较大小,再将大的那个值存入第三个参数指定的内存中(*(Xn|SP) = MAX(*(Xn|SP), Xs)),并且保证这些步骤都是原子的。大小比较的时候,将这个数值作为有符号数比。而LDUMAX指令,顾名思义,和LDSMAX指令功能基本相同,只是比较大小的时候,将这个数值作为无符号数比。

有比较过后将较大的值存入的指令,那就一定会有比较过后将较小的值存入的指令:

LDSMIN <Ws>, <Wt>, [<Xn|SP>]
LDSMIN <Xs>, <Xt>, [<Xn|SP>]

LDUMIN <Ws>, <Wt>, [<Xn|SP>]
LDUMIN <Xs>, <Xt>, [<Xn|SP>]

STSMIN <Ws>, [<Xn|SP>]
STSMIN <Xs>, [<Xn|SP>]

STUMIN <Ws>, [<Xn|SP>]
STUMIN <Xs>, [<Xn|SP>]

交换原子操作

SWP <Ws>, <Wt>, [<Xn|SP>]
SWP <Xs>, <Xt>, [<Xn|SP>]

SWP指令从第三个参数,也就是Xn或SP寄存器指定的内存位置读出32位或64位的值,将其存放进第二个参数,也就是Wt或Xt寄存器中,然后再将第一个参数,也就是Ws或Xs寄存器中的值存入第三个参数指定的内存中,并且保证这些步骤都是原子的。

比较交换原子操作

CAS <Ws>, <Wt>, [<Xn|SP>{,#0}]
CAS <Xs>, <Xt>, [<Xn|SP>{,#0}]

CASP <Ws>, <W(s+1)>, <Wt>, <W(t+1)>, [<Xn|SP>{,#0}]
CASP <Xs>, <X(s+1)>, <Xt>, <X(t+1)>, [<Xn|SP>{,#0}]

CAS指令从第三个参数,也就是Xn或SP寄存器指定的内存位置读出32位或64位的值,然后再将这个读出的值和第一个参数,也就是Ws或Xs寄存器中的值进行比较,如果它们相同的话,就把第二个参数,也就是Wt或Xt寄存器中的值存入第三个参数指定的内存中,最后不管前面比较的结果相不相同,都需要将前面读取出来的内存位置的原始值存入第一个参数指定的寄存器中,并且保证这些步骤都是原子的。

CASP也是比较交换原子操作,多出来的P表示Pair。和CAS不同的是,它一次性操作两个连续成对的寄存器。

前面介绍的都是基本的原子操作,操作的寄存器都是32位或64位的,并且没有任何内存屏障的语义。

在上面的基本操作基础上,ARMv8.1还提供了带Load-Acquire或Store-Release单向内存屏障语义的指令。具体来说,如果想在一条基本的原子操作指令上加上Load-Acquire语义,可以在基本指令后面加上A;而如果想在一条基本的原子操作指令上加上Store-Release语义,可以在基本指令后面加上L;还可以两个都加,可以在基本指令后面同时加上AL,那就等同于一个数据内存屏障。

例如,对于LDADD指令来说,有如下自带内存屏障语义的版本:

LDADDA <Xs>, <Xt>, [<Xn|SP>]
LDADDAL <Xs>, <Xt>, [<Xn|SP>]
LDADDL <Xs>, <Xt>, [<Xn|SP>]

但是,对于以ST打头的指令,由于它们不会返回从内存中读取出来的值,所以不需要Load-Acquire语义,就没有包含L的版本。

例如,对于STADD指令,只提供下面一个带Store-Release的版本:

STADDL <Xs>, [<Xn|SP>]

还有,前面说的基本指令都是操作32位或64位数的,如果想操作16位的数,需要在基本指令后面加上H(Halfword);而如果想操作8位的数,需要在基本指令后面加上B(Byte)。

如果原子操作指令又要包含Load-Acquire或Store-Release单向内存屏障语义,又要操作8位或16位的数,那么在基本原子操作指令的后面,先添加表示单向内存屏障语义的A或L,后添加表示操作数位数的B或H。

例如,还是对于基本的LDADD指令,如果想操作8位的数,则有如下版本:

LDADDB <Ws>, <Wt>, [<Xn|SP>]
LDADDAB <Ws>, <Wt>, [<Xn|SP>]
LDADDLB <Ws>, <Wt>, [<Xn|SP>]
LDADDALB <Ws>, <Wt>, [<Xn|SP>]
Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐