ARM下的CAS

简单分享记录一下最近在ARM下探索的CAS(compare and swap)实现,语言使用的是C++。我们知道ARM是RISC精简指令集,它不像x86下面有cmpxchg这么一条指令就可以做到,我们需要用多条指令来锁内存、写入值等操作。

这里是我根据参考资料写的一个弱CAS操作函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// Atomic compare and swap, return 0 if success, otherwise return 1
static inline unsigned atomic_compare_and_swap(volatile unsigned* ptr,
                                               unsigned old_value,
                                               unsigned new_value) {
  asm volatile(
      "ldaxr %w0, [%1]\n"
      "cmp %w0, %w2\n"
      "b.ne 1f\n"
      "stlxr %w0, %w3, [%1]\n"
      "1: clrex"
      : "=&r"(old_value)
      : "r"(ptr), "r"(old_value), "r"(new_value)
      : "memory");

  return old_value;
}

GPT-4 对这段代码的理解

这段代码看起来是一个用于 ARM 架构的 CAS(Compare and Swap)操作的实现。CAS 是一种原子操作,用于在多线程环境中确保数据的一致性。这段代码使用了 ARMv8-A 架构的汇编指令。现在我来解释这段代码的每一条汇编指令:

ldaxr %w0, [%1]: 这是一条 Load-Acquire Exclusive Register 指令,将内存地址 [%1](ptr 指针所指向的内存)中的值加载到寄存器 %w0(old_value)中。这是一个独占加载操作,表示此操作会尝试获取一个独占访问权限,以确保后续的操作不会被其他处理器或内核干扰。

cmp %w0, %w2: 这是一条 Compare 指令,将寄存器 %w0(old_value)和寄存器 %w2(old_value)的值进行比较。注意这里的 %w0 和 %w2 都表示 old_value,但在第一条指令之后,%w0 的值实际上是从内存加载的值。

b.ne 1f: 这是一条 Branch 指令,当上一条 Compare 指令的结果不等(Not Equal)时,跳转到标签 1(即下面的 1: clrex)。如果从内存加载的值不等于预期的 old_value,这将跳过后续的 Store-Exclusive 操作并清除独占访问权限。

stlxr %w0, %w3, [%1]: 这是一条 Store-Exclusive Register 指令。如果独占访问权限仍然有效,那么将寄存器 %w3(new_value)的值存储到内存地址 [%1](ptr 指针所指向的内存)。同时,将存储是否成功的结果写入寄存器 %w0。如果存储成功,%w0 的值将为 0,否则为非零值。

1: clrex: 这是一条 Clear Exclusive 指令,用于清除当前处理器的独占访问权限。这样可以确保后续不再需要独占访问权限的指令不会受到影响。

这段代码看起来没有问题。当调用这个函数时,如果在内存地址 ptr 的值等于 old_value,那么将会把 new_value 存储到该内存地址,并返回原始的 old_value。如果在内存地址 ptr 的值不等于 old_value,那么什么都不会发生,直接返回从内存加载的值。

我们再来看看这段代码

有些细节是在我写这个函数的时候不太明白的,也查阅了一些资料,这里跟着说明一下。

asm后面的volatile告诉编译器禁用对这段汇编的优化,保持原样。汇编代码结束后第一行我们定义了有哪些OutputOperands,形如“=&r”叫constraint,这里的=表明会覆盖已有变量的值,&表明这个值在指令使用完InputOperands前就会被写入,r表明使用一个通用寄存器。

这里我们没有对传入的值使用asmSymbolicName,因此每个操作数和汇编里面的从零开始的操作数一一对应:

  • old_value对应了%0
  • ptr对应了%1
  • old_value对应了%2
  • new_value对应%3

可以看到按照顺序可以对应。最后我们编译器我们改变了内存,这样编译器可以处理好读写内存的屏障。

这里和x86有一点不同,如果写入成功,我们的函数返回0,否则返回1。

参考资料