AtomicExpandPass

基本概念

INITIALIZE_PASS_BEGIN(AtomicExpandLegacy, DEBUG_TYPE,  
                      "Expand Atomic instructions", false, false)  
INITIALIZE_PASS_DEPENDENCY(TargetPassConfig)  
INITIALIZE_PASS_END(AtomicExpandLegacy, DEBUG_TYPE,  
                    "Expand Atomic instructions", false, false)

本文件定义了一个 IR 级别的 Pass,用于将原子指令替换为__atomic_* 库函数调用,或者替换为实现相同语义、但更适合目标后端的特定指令。这些替代方式可能包括:基于 intrinsic 的 load-linked/store-conditional 循环、AtomicCmpXchg,或者类型强制转换等。
典型的原子指令如下:

@x = global i32 0, align 4

define void @atomic_add_example() {
entry:
  %ptr = load ptr, ptr @x
  %val = atomicrmw add ptr @x, i32 1 seq_cst
  ret void
}
define i32 @atomic_load() {
entry:
  %val = load atomic i32, i32* @x seq_cst
  ret i32 %val
}

我们可能会提出下面几个问题:

  • 原子指令哪来的?
  • 原子指令作用是干什么?
  • 原子指令怎么降级的?
  • 哪些指令可能会有原子指令?
    下面来一一回答:
原子指令哪来的?

当我们用 C/C++ 写并发程序时,如果代码里用了像 std::atomic<int> x; x.fetch_add(1); 或者 __atomic_load, __atomic_store 这些原子操作,Clang/LLVM 前端就会在生成 LLVM IR 的过程中插入对应的 Atomic IR 指令。
例如:

std::atomic<int> x;
int y = x.load (std::memory_order_seq_cst);

对应的 LLVM IR 可能是:

%y = load atomic i32, i32* @x seq_cst

某些高级并发分析/优化 Pass 也会把普通的内存访问改写为原子访问,比如实现自旋锁的 Pass,可能会把普通的 load + cmpxchg 模式插成 atomicrmw。

原子指令的作用是什么?

在并发/多线程环境下,原子指令就是用来保证“多个线程同时访问同一块内存时”的一致性和可见性,具体包括:

  1. 原子性(Atomicity)
    • 一条原子操作要么全部执行完毕,要么一个也不执行;在多核环境下,当一个线程对同一个内存地址做原子读写时,别的线程不会看到一个“半成品”的状态。
    • 例如:atomicrmw add i32* @x, 1 要保证对 @x 的“读取并加 1 再写回”这一整个过程不会被打断。
  2. 可见性(Visibility)
    • 通过指定不同的 MemoryOrdering(如 monotonic、acquire、release、seq_cst),可以向编译器/CPU 明确说明:这条原子指令要满足“访问前后需要插入 barrier/fence”,或“加载要保证拿到最新值”,或“存储以后才看到新值”之类。
    • LLVM IR 的 atomic load/store/rmw/cmpxchg 会带上一个 AtomicOrdering(比如 seq_cst、acquire、release、monotonic),让后端知道需要插入什么级别的内存屏障。
  3. 排序(Ordering)
    • 不同的 ordering 级别控制了“该原子操作之前的读写和之后的读写”在编译器/CPU 层面的重排序行为。
    • 例如 seq_cst 要求全局的强顺序,一旦执行,所有 CPU 核心都看到同一个全序;而 monotonic 最弱,只保证对同一个地址的原子读写具备原子性,不保证跨地址的重排序。
      简言之,原子指令让多线程程序在“共享内存读写”时,可以安全地做加/减/比较-交换等操作,不出现数据竞争或不可预测的“半写入”状态
原子指令怎么降级的?

首先可能用 __atomic_*__sync_* 库函数,如果目标架构不提供某种原子指令,LLVM 则式样 libatomic 库函数。
例如将:

%old = atomicrmw add i32, i32* @x, i32 1 seq_cst

降级为:

declare i32 @__atomic_fetch_add(i32* %ptr, i32 %val, i32 %ordering)

; 在 IR 里变成:
%old = call i32 @__atomic_fetch_add(i32* @x, i32 1, i32 5)  ; 5=__ATOMIC_SEQ_CST

这类调用后端会链接到 libatomic(或 libgcc)的实现,由运行时库使用操作系统提供的原语(如 Linux futex 或一条可用的 CAS 指令)来完成。

即完全交给链接时了,但是会多调用开销

哪些指令可能会有原子指令?
  • load、store
  • rmv(读/改/写操作)
  • cmpchg(compare-and-swap)
  • fence(内存屏障/栅栏指令)
  • xchg(特殊的交换指令)
  • 其他内建

可以去具体函数:performMaskedAtomicOp