riscv和loongarch原子指令对比
riscv的lr/sc指令
lr和sc指令介绍
LR指令是load reserved的缩写,读取保留;SC指令是store condition的缩写,条件存储。
LR指令的指令格式如下:
lr.{w,d}.{aqrl} rd, (rs1)
lr指令从rs1处加载内容到rd寄存器,然后在rs1对应地址上设置保留标记(reservation set)。其中w和d分别对应32位和64位版本。
SC指令的指令格式如下
sc.{w,d}.{aqrl} rd, rs2, (rs1)
sc指令把rs2写到rs1地址之前,会先判断rs1内存地址是否设置了“保留标记”,如果设置了,则把rs2值正常写道rs1的内存地址当中,并将rd寄存器设置为0,表示保存成功。如果rs1内存地址没有设置“保留标记”,则不保存,并把rd设置为0。我们用伪代码描述一下sc.d指令:
if is_reserved(rs1):
*rs1 = rs2
rd = 0
else:
rd = 1
riscv的spec中只规定了,sc如果执行失败,会写入非零到rd,不一定为1。
对于 lr/sc 指令,要求 rs1 寄存器中的地址是按宽度对齐的,比如 lr.w 要求 4 字节对齐,sc.d 要求 8 字节对齐。否则会触发非对齐异常。
如果在 sc 指令之前,当前 hart 观察到了对应内存地址被其他 hart 写了,则 sc 指令会失败。相当于保留标记失效了。如果对应内存地址被外部设备(非 hart)或者总线写了,外部设备需要主动把写范围内的保留标记清除,不在写入范围的字节不需要清除保留标记。
什么是cas
比较并交换(compare and swap, CAS),是原子操作的一种,可用于在多线程编程中实现不被打断的数据交换操作,从而避免多线程同时改写某一数据时由于执行顺序不确定性以及中断的不可预知性产生的数据不一致问题。 该操作通过将内存中的值与指定数据进行比较,当数值一样时将内存中的数据替换为新的值。
int cas(long *addr, long old, long new)
{
/* Executes atomically. */
if(*addr != old)
return 0;
*addr = new;
return 1;
}
实现cas
// https://github.com/bminor/musl/blob/v1.2.4/arch/riscv64/atomic_arch.h
static inline int a_cas(volatile int *p, int t, int s)
{
int old, tmp;
__asm__ __volatile__ (
"\n1: lr.w.aqrl %0, (%2)\n"
" bne %0, %3, 1f\n"
" sc.w.aqrl %1, %4, (%2)\n"
" bnez %1, 1b\n"
"1:"
: "=&r"(old), "=&r"(tmp)
: "r"(p), "r"((long)t), "r"((long)s)
: "memory");
return old;
}
- 这里
t代表了expected vale或者说old value对应于汇编中的%3,s代表了new value在汇编中是%4,p代表了需要写入的内存地址,对应于%2 - 局部变量
old表示存储在p中的值,在汇编中为%0,局部变量tmp表示的是sc操作的返回值,在汇编中为%1
上面代码的可读性较差,参考内核代码修改如下:
static inline int cas(volatile int *p, int expected, int new)
{
int prev, rc;
__asm__ __volatile__ (
"0: lr.w %[prev], (%[p])\n"
" bne %[prev], %[e], 1f\n"
" sc.w %[rc], %[new], (%[p])\n"
" bnez %[rc], 0b\n"
" fence rw, rw\n"
"1:"
: [prev]"=&r"(prev), [rc]"=&r"(rc)
: [p]"r"(p), [e]"r"(expected), [new]"r"(new)
: "memory");
return prev;
}
汇编代码的解释如下:
| 代码 | 解释 |
|---|---|
lr.w %[prev], (%[p]); bne %[prev], %[e], 1f |
if expected != (*p): jump fail(1) |
sc.w %[rc], %[new], (%[p]); bnez %[rc], 0b |
if (expected == (*p) && sc_failed): jump retry(0) |
- 如果是
expected==*p失败,cas会直接判定为失败, - 如果是
sc操作失败,cas会retry,直到expected!=*p或者sc成功
loongarch的ll/sc指令
指令说明
LL指令的格式如下:
ll.{w,d} rd, rj, si14
ll指令从(rj+si14)处加载内容到rd寄存器,然后在(rj+si14)对应地址上设置保留标记(LLbit=1)。
SC指令的格式如下:
sc.{w,d} rd, rj, si14
sc指令负责将rd的内容写入到(rj+si14)对应地址上,仅仅当LLbit=1时才会真正发生写操作。sc指令无论成功与否都会将sc命令执行时的LLbit的值写入rd,也就是说执行成功返回1,失败返回0。
实现cas
// 参考
int cas(volatile int *p, int expected, int new)
{
int prev, rc_tmp;
__asm__ __volatile__ (
"0: ll.w %[prev], %[v]\n"
" bne %[prev], %[e], 1f\n"
" or %[rc_t], %[new], $zero\n"
" sc.w %[rc_t], %[v]\n"
" beqz %[rc_t], 0b\n"
" dbar 0\n"
"1:"
: [prev]"=&r"(prev), [rc_t]"=&r"(rc_tmp), [v]"=ZB"(*p)
: [e]"r"(expected), [new]"r"(new)
: "memory");
return prev;
}
rc_tmp既要承接ll/sc的返回值,又要承载new的值
关于early clobber(&)约束
来自stackoveflow的回答:
By default, the compiler assumes all inputs will be consumed before any output registers are written to, so that it's allowed to use the same registers for both. This leads to better code when possible, but if the assumption is wrong, things will fail catastrophically. The "early clobber" marker is a way to tell the compiler that this output will be written before all the input has been consumed, so it cannot share a register with any input.
默认情况下,对于内联汇编的处理,编译器会假定:“对于输出寄存器的写入发生之前,所有的输入已经被消费”,这样允许使用相同的寄存器作为输入和输出。通常这样会产生更好的汇编代码,而early clobber(&)标记是在告诉编译器,这个输出的写操作发生在输入被消耗之前,因此这个写操作数,不能和输入进行寄存器共享。(PS:gcc认为如果当前这个寄存器发生过写操作,代表后面寄存器的分配是可以使用该寄存器的,gcc不会关注语义,如果寄存器上发生了写操作,以为着该寄存器可以再次被分配了)
GNU C inline asm syntax was designed to wrap a single instruction as efficiently as possible. You can put multiple instructions in an asm template, but the defaults (assuming that all inputs are read before any outputs are written) are designed around wrapping a single instruction.
"GUN C"内联汇编被设计是专门用来包装单条汇编指令的。你也可以在asm模板中防止多条指令,但是默认情况下它还是认为"读在写先"
AMO 指令
AMO 是 Atomic Memory Operation 的缩写。
riscv amo指令
AMO 指令有如下几个:
| AMO 指令 | 格式 | 说明 |
|---|---|---|
| AMOSWAP | amoswap.{w/d}.{aqrl} rd, rs2, (rs1) | 原子交换指令,rd = *rs1, *rs1 = rs2 |
| AMOADD | amoadd.{w/d}.{aqrl} rd, rs2, (rs1) | 原子加法指令,rd = *rs1, *rs1 += rs2 |
| AMOAND | amoand.{w/d}.{aqrl} rd, rs2, (rs1) | 原子按位与指令,rd = *rs1, *rs1 &= rs2 |
| AMOOR | amoor.{w/d}.{aqrl} rd, rs2, (rs1) | 原子按位或指令,rd = *rs1, *rs1 |= rs2 |
| AMOXOR | amoxor.{w/d}.{aqrl} rd, rs2, (rs1) | 原子按位异或指令,rd = *rs1, *rs1 ^= rs2 |
| AMOMAX | amomax.{w/d}.{aqrl} rd, rs2, (rs1) | 原子有符号取最大值指令,rd = rs1, *rs1 = max(rs1, rs2) |
| AMOMAXU | amomaxu.{w/d}.{aqrl} rd, rs2, (rs1) | 原子无符号取最大值指令,rd = rs1, *rs1 = maxu(rs1, rs2) |
| AMOMIN | amomin.{w/d}.{aqrl} rd, rs2, (rs1) | 原子有符号取最小值指令,rd = rs1, *rs1 = min(rs1, rs2) |
| AMOMINU | amominu.{w/d}.{aqrl} rd, rs2, (rs1) | 原子无符号取最小值指令,rd = rs1, *rs1 = minu(rs1, rs2) |
- 使用方式
int atomic_add(int i, int* p){
// 这里v可以看作是值引用
__asm__ __volatile__ (
"amoadd.w zero, %[i], %[v]"
:[v]"+A"(*p)
:[i]"r"(i)
:"memory");
}
loongarch am指令
| AMO 指令 | 格式 | 说明 |
|---|---|---|
| AMSWAP | amswap{_db}.{w/d} rd, rk, rj | 原子交换指令,rd = *rj, *rj = rk |
| AMADD | amadd{_db}.{w/d} rd, rk, rj | 原子加法指令,rd = *rj, *rj += rk |
| AMAND | amand{_db}.{w/d} rd, rk, rj | 原子按位与指令,rd = *rj, *rj &= rk |
| AMOR | amor.{w/d}.{aqrl} rd, rk, rj | 原子按位或指令,rd = *rj, *rj |= rk |
| AMXOR | amoxor.{w/d}.{aqrl} rd, rk, rj | 原子按位异或指令,rd = *rj, *rj ^= rk |
| AMMAX | amomax.{w/d}.{aqrl} rd, rk, rj | 原子有符号取最大值指令,rd = rj, *rj = max(rk, *rj) |
| AMMAXU | amomaxu.{w/d}.{aqrl} rd, rk, rj | 原子无符号取最大值指令,rd = rj, *rj = maxu(rk, *rj) |
| AMMIN | amomin.{w/d}.{aqrl} rd, rk, rj | 原子有符号取最小值指令,rd = rj, *rj = min(rk, *rj) |
| AMMINU | amominu.{w/d}.{aqrl} rd, rk, rj | 原子无符号取最小值指令,rd = rj, *rj = minu(rk, *rj) |
loongarch汇编表达上,没有
(r)寻址的方式
- demo
int atomic_add(int i, int* p){
// 这里v可以看作是值引用
__asm__ __volatile__ (
"amadd.w $zero, %[i], %[v]"
:[v]"+ZB"(*p)
:[i]"r"(i)
:"memory");
}

浙公网安备 33010602011771号