LLVM 原子指令和并发指南

简介

LLVM 支持在线程和异步信号存在的情况下良好定义的指令。

原子指令专门设计用于为以下内容提供可读的 IR 和优化的代码生成

  • C++ <atomic> 头文件和 C <stdatomic.h> 头文件。这些最初在 C++11 和 C11 中添加。内存模型随后进行了调整,以纠正初始规范中的错误,因此 LLVM 目前计划实现 C++20 指定的版本。(请参阅 C++20 草案标准或非官方的 最新 C++ 草案C2x 草案也可用,尽管文本尚未更新为 C++20 纠正的勘误表。)

  • Java 风格内存的正确语义,包括 volatile 和常规共享变量。(Java 规范

  • gcc 兼容的 __sync_* 内建函数。(描述

  • 具有原子语义的其他场景,包括 C++ 中具有非平凡构造函数的 static 变量。

IR 中的原子性和 volatile 是正交的;“volatile” 是 C/C++ volatile,它确保每个 volatile 加载和存储都会发生,并按声明的顺序执行。几个例子:如果一个 SequentiallyConsistent 存储之后立即跟随着另一个对相同地址的 SequentiallyConsistent 存储,则第一个存储可以被删除。这种转换对于一对 volatile 存储是不允许的。另一方面,非 volatile 非原子加载可以自由地跨越 volatile 加载移动,但 Acquire 加载则不行。

本文档旨在为任何为 LLVM 编写前端或在 LLVM 上进行优化传递工作的人员提供指南,指导如何在并发存在的情况下处理具有特殊语义的指令。这并非旨在成为语义的精确指南;细节可能变得极其复杂和难以理解,并且通常不是必需的。

原子操作外的优化

基本的 'load''store' 允许各种优化,但在并发环境中可能导致未定义的结果;请参阅 NotAtomic。本节专门讨论在并发环境中适用的一项优化器限制,该限制获得了更详细的描述,因为任何处理存储的优化都需要意识到这一点。

从优化器的角度来看,规则是如果没有涉及任何具有原子排序的指令,则并发无关紧要,但有一个例外:如果一个变量可能对另一个线程或信号处理程序可见,则不能沿着可能不会执行的路径插入存储。考虑以下示例

/* C code, for readability; run through clang -O2 -S -emit-llvm to get
    equivalent IR */
 int x;
 void f(int* a) {
   for (int i = 0; i < 100; i++) {
     if (a[i])
       x += 1;
   }
 }

以下在非并发情况下是等效的

int x;
void f(int* a) {
  int xtemp = x;
  for (int i = 0; i < 100; i++) {
    if (a[i])
      xtemp += 1;
  }
  x = xtemp;
}

但是,LLVM 不允许将前者转换为后者:如果另一个线程可以同时访问 x,则可能会间接引入未定义的行为。该线程将读取 undef 而不是它期望的值,这可能会导致后续的未定义行为。(这个例子特别令人感兴趣,因为在并发模型实现之前,LLVM 会执行这种转换。)

请注意,允许推测性加载;作为竞争一部分的加载返回 undef,但不会产生未定义的行为。

原子指令

对于简单加载和存储不足的情况,LLVM 提供了各种原子指令。提供的确切保证取决于排序;请参阅 原子排序

load atomicstore atomic 提供与非原子加载和存储相同的基本功能,但在涉及线程和信号的情况下提供额外的保证。

cmpxchgatomicrmw 本质上类似于原子加载后跟原子存储(对于 cmpxchg 存储是条件性的),但在加载和存储之间,任何线程上都不会发生其他内存操作。

fence 提供不属于其他操作的 Acquire 和/或 Release 排序;它通常与 Monotonic 内存操作一起使用。Monotonic 加载后跟 Acquire fence 大致等同于 Acquire 加载,而 Monotonic 存储后跟 Release fence 大致等同于 Release 存储。SequentiallyConsistent fence 的行为类似于 Acquire 和 Release fence,并且还提供具有一些复杂保证的总排序,有关详细信息,请参阅 C++ 标准。

生成原子指令的前端通常需要在一定程度上了解目标;原子指令保证是无锁的,因此,宽度大于目标本地支持的指令可能无法生成。

原子排序

为了在性能和必要的保证之间取得平衡,有六个原子性级别。它们按强度顺序列出;每个级别都包含前一个级别的所有保证,除了 Acquire/Release。(另请参阅 LangRef 排序。)

NotAtomic

NotAtomic 是显而易见的,即非原子的加载或存储。(这实际上不是原子性级别,但在此处列出以进行比较。)这本质上是常规的加载或存储。如果给定内存位置存在竞争,则从该位置的加载返回 undef。

相关标准

这旨在匹配 C/C++ 中的共享变量,并用于任何其他需要内存访问且不可能发生竞争的上下文中。(精确的定义在 LangRef 内存模型中。)

前端注意事项

规则本质上是,多个线程使用基本加载和存储访问的所有内存都应受到锁或其他同步机制的保护;否则,您很可能遇到未定义的行为。如果您的前端用于像 Java 这样的“安全”语言,请使用 Unordered 来加载和存储任何共享变量。请注意,NotAtomic volatile 加载和存储不是真正的原子操作;不要尝试将它们用作替代品。(根据 C/C++ 标准,volatile 确实为异步信号提供了一些有限的保证,但原子操作通常是更好的解决方案。)

优化器注意事项

允许在代码路径中引入对共享变量的加载,即使它们原本不存在;不允许引入对共享变量的存储。请参阅 原子操作外的优化

代码生成注意事项

这里一个有趣的限制是,不允许写入存储相关字节之外的字节。这主要与未对齐的存储相关:通常不允许将未对齐的存储转换为两个与未对齐的存储宽度相同的对齐存储。后端也应生成 i8 存储作为 i8 存储,而不是写入周围字节的指令。(如果您正在为无法满足这些限制且关心并发的架构编写后端,请发送电子邮件至 llvm-dev。)

Unordered

Unordered 是最低级别的原子性。它本质上保证竞争会产生某种程度上的合理结果,而不是产生未定义的行为。它还保证操作是无锁的,因此它不依赖于数据是特殊原子结构的一部分,也不依赖于单独的每个进程全局锁。请注意,对于不支持的原子操作,代码生成将失败;如果您需要此类操作,请使用显式锁定。

相关标准

这旨在匹配 Java 内存模型中的共享变量。

前端注意事项

这不能用于同步,但对于 Java 和其他需要保证生成的代码永远不会表现出未定义行为的“安全”语言很有用。请注意,对于本机宽度的加载,此保证在常见平台上是廉价的,但对于更宽的加载(例如 ARM 上的 64 位存储)可能很昂贵或不可用。(Java 或其他“安全”语言的前端通常会将 ARM 上的 64 位存储拆分为两个 32 位无序存储。)

优化器注意事项

就优化器而言,这禁止任何将单个加载转换为多个加载、将存储转换为多个存储、缩小存储或存储原本不会存储的值的转换。不安全优化的示例包括将赋值缩小为位域、重新物化加载以及将加载和存储转换为 memcpy 调用。但是,重新排序无序操作是安全的,并且优化器应利用这一点,因为无序操作在需要它们的语言中很常见。

代码生成注意事项

这些操作必须是原子的,因为如果您使用无序加载和无序存储,则加载无法看到从未存储的值。通常的加载或存储指令通常就足够了,但请注意,无序加载或存储不能拆分为多个指令(或执行多个内存操作的指令,例如没有 LPAE 的 ARM 上的 LDRD,或 LPAE ARM 上非自然对齐的 LDRD)。

Monotonic

Monotonic 是可以用于同步原语的最弱原子性级别,尽管它不提供任何通用同步。它本质上保证,如果您获取影响特定地址的所有操作,则存在一致的排序。

相关标准

这对应于 C++/C memory_order_relaxed;有关确切定义,请参阅这些标准。

前端注意事项

如果您正在编写直接使用此功能的前端,请谨慎使用。同步方面的保证非常弱,因此请确保这些仅在您知道正确的模式中使用。通常,这些将用于不保护其他内存的原子操作(如原子计数器),或与 fence 一起使用。

优化器注意事项

就优化器而言,这可以被视为相关内存位置上的读+写(并且别名分析将利用这一点)。此外,允许在 Monotonic 加载周围重新排序非原子和 Unordered 加载。允许 CSE/DSE 和一些其他优化,但 Monotonic 操作不太可能以使这些优化有用的方式使用。

代码生成注意事项

代码生成基本上与加载和存储的无序代码生成相同。不需要 fence。cmpxchgatomicrmw 必须作为单个操作出现。

Acquire

Acquire 提供了一种屏障,这种屏障对于获取锁以使用普通加载和存储访问其他内存是必需的。

相关标准

这对应于 C++/C memory_order_acquire。它也应该用于 C++/C memory_order_consume

前端注意事项

如果您正在编写直接使用此功能的前端,请谨慎使用。Acquire 仅在与 Release 操作配对时才提供语义保证。

优化器注意事项

不了解原子操作的优化器可以将此视为不抛出异常的调用。也可以将存储从 Acquire 加载或读-修改-写操作之前移动到之后,并将非 Acquire 加载从 Acquire 操作之前移动到之后。

代码生成注意事项

具有弱内存排序的架构(基本上是当今除 x86 和 SPARC 以外的所有架构)需要某种 fence 来维护 Acquire 语义。所需的精确 fence 因架构而异,但对于简单的实现,大多数架构都提供了一个足够强大的屏障来满足所有需求(ARM 上的 dmb,PowerPC 上的 sync 等)。在等效的 Monotonic 操作之后放置这样的 fence 足以维护内存操作的 Acquire 语义。

Release

Release 类似于 Acquire,但具有释放锁所需的屏障类型。

相关标准

这对应于 C++/C memory_order_release

前端注意事项

如果您正在编写直接使用此功能的前端,请谨慎使用。Release 仅在与 Acquire 操作配对时才提供语义保证。

优化器注意事项

不了解原子操作的优化器可以将此视为不抛出异常的调用。也可以将加载从 Release 存储或读-修改-写操作之后移动到之前,并将非 Release 存储从 Release 操作之后移动到之前。

代码生成注意事项

请参阅关于 Acquire 的部分;在相关操作之前放置 fence 通常足以满足 Release。请注意,store-store fence 不足以实现 Release 语义;store-store fence 通常不会暴露给 IR,因为它们非常难以正确使用。

AcquireRelease

AcquireRelease(IR 中的 acq_rel)提供 Acquire 和 Release 屏障(对于 fence 和既读取又写入内存的操作)。

相关标准

这对应于 C++/C memory_order_acq_rel

前端注意事项

如果您正在编写直接使用此功能的前端,请谨慎使用。Acquire 仅在与 Release 操作配对时才提供语义保证,反之亦然。

优化器注意事项

一般来说,优化器应将此视为不抛出异常的调用;可能的优化通常不有趣。

代码生成注意事项

此操作具有 Acquire 和 Release 语义;请参阅关于 Acquire 和 Release 的部分。

SequentiallyConsistent

SequentiallyConsistent(IR 中的 seq_cst)为加载提供 Acquire 语义,为存储提供 Release 语义。此外,它保证所有 SequentiallyConsistent 操作之间存在总排序。

相关标准

这对应于 C++/C memory_order_seq_cst、Java volatile 和未另行指定的 gcc 兼容的 __sync_* 内建函数。

前端注意事项

如果前端正在公开原子操作,那么程序员更容易理解这些操作,并且使用它们通常是一种实用的性能权衡。

优化器注意事项

不了解原子操作的优化器可以将此视为不抛出异常的调用。对于 SequentiallyConsistent 加载和存储,允许与 Acquire 加载和 Release 存储相同的重新排序,但 SequentiallyConsistent 操作可能不会重新排序。

代码生成注意事项

SequentiallyConsistent 加载至少需要与 Acquire 操作相同的屏障,而 SequentiallyConsistent 存储需要 Release 屏障。此外,代码生成器必须强制执行 SequentiallyConsistent 存储后跟 SequentiallyConsistent 加载之间的排序。这通常通过在加载之前发出完整的 fence 或在存储之后发出完整的 fence 来完成;哪种方法更合适因架构而异。

原子操作与 IR 优化

供优化器编写者查询的谓词

  • isSimple():非 volatile 或原子的加载或存储。例如,memcpyopt 会检查它可能转换的操作。

  • isUnordered():非 volatile 且最多为 Unordered 的加载或存储。例如,LICM 在提升操作之前会检查此项。

  • mayReadFromMemory()/mayWriteToMemory():现有谓词,但请注意,它们对于任何 volatile 或至少为 Monotonic 的操作都返回 true。

  • isStrongerThan / isAtLeastOrStrongerThan:这些是关于排序的谓词。它们对于了解原子操作的 pass 很有用,例如在单个原子访问中执行 DSE,而不是跨越 release-acquire 对(有关示例,请参阅 MemoryDependencyAnalysis)

  • 别名分析:请注意,AA 将为任何 Acquire 或 Release 以及任何 Monotonic 操作访问的地址返回 ModRef。

为了支持围绕原子操作进行优化,请确保您正在使用正确的谓词;如果这样做,一切都应该正常工作。如果您的 pass 应该优化某些原子操作(特别是 Unordered 操作),请确保它不会将原子加载或存储替换为非原子操作。

优化如何与各种原子操作交互的一些示例

  • memcpyopt:原子操作无法优化为 memcpy/memset 的一部分,包括无序加载/存储。它可以将操作拉过某些原子操作。

  • LICM:Unordered 加载/存储可以移出循环。它只是将 monotonic 操作视为对内存位置的读+写,并将任何比这更严格的操作视为不抛出异常的调用。

  • DSE:Unordered 存储可以像普通存储一样进行 DSE。Monotonic 存储在某些情况下可以进行 DSE,但这很难推理,而且不是特别重要。在某些情况下,DSE 可以在更强的原子操作中操作,但这相当棘手。DSE 将此推理委托给 MemoryDependencyAnalysis(其他 pass(如 GVN)也使用它)。

  • 折叠加载:来自常量全局变量的任何原子加载都可以常量折叠,因为它无法被观察到。类似的推理允许对原子加载和存储使用 sroa。

原子操作与代码生成

原子操作在 SelectionDAG 中用 ATOMIC_* 操作码表示。在对所有原子排序使用屏障指令的架构(如 ARM)上,如果 shouldInsertFencesForAtomic() 返回 true,则 AtomicExpand Codegen pass 可以发出适当的 fence。

所有原子操作的 MachineMemOperand 当前都标记为 volatile;这在 IR 的 volatile 意义上是不正确的,但 CodeGen 非常保守地处理任何标记为 volatile 的内容。这应该在某个时候得到修复。

原子操作的一个非常重要的属性是,如果您的后端支持任何给定大小的内联无锁原子操作,您应该以无锁方式支持该大小的所有操作。

当目标实现原子 cmpxchg 或 LL/SC 指令(大多数都这样做)时,这很简单:所有其他操作都可以在这些原语之上实现。但是,在许多较旧的 CPU(例如 ARMv5、SparcV8、Intel 80386)上,存在原子加载和存储指令,但没有 cmpxchg 或 LL/SC。由于使用本机指令实现 atomic load 是无效的,但使用库调用调用使用互斥锁的函数来实现 cmpxchg 是有效的,因此 atomic load 也必须在此类架构上扩展为库调用,以便它可以保持原子性,相对于同时发生的 cmpxchg,通过使用相同的互斥锁。

AtomicExpandPass 可以帮助实现这一点:它会将所有原子操作扩展为正确的 __atomic_* 库调用,对于任何大于 setMaxAtomicSizeInBitsSupported 设置的最大大小(默认为 0)的大小。

在 x86 上,所有原子加载都生成 MOV。SequentiallyConsistent 存储生成 XCHG,其他存储生成 MOV。SequentiallyConsistent fence 生成 MFENCE,其他 fence 不会导致生成任何代码。cmpxchg 使用 LOCK CMPXCHG 指令。atomicrmw xchg 使用 XCHGatomicrmw addatomicrmw sub 使用 XADD,所有其他 atomicrmw 操作都生成一个带有 LOCK CMPXCHG 的循环。根据结果的用户,一些 atomicrmw 操作可以转换为类似 LOCK AND 的操作,但这通常不起作用。

在 ARM(v8 之前)、MIPS 和许多其他 RISC 架构上,Acquire、Release 和 SequentiallyConsistent 语义需要为每个此类操作提供屏障指令。加载和存储生成普通指令。cmpxchgatomicrmw 可以使用带有 LL/SC 风格指令的循环来表示,这些指令在缓存行上采用某种互斥锁(ARM 上的 LDREXSTREX 等)。

后端通常最容易使用 AtomicExpandPass 来降低某些原子构造。以下是它可以执行的一些降低

  • cmpxchg -> 通过覆盖 shouldExpandAtomicCmpXchgInIR()emitLoadLinked()emitStoreConditional(),使用 load-linked/store-conditional 的循环

  • 大型加载/存储 -> 通过覆盖 shouldExpandAtomicStoreInIR()/shouldExpandAtomicLoadInIR(),使用 ll-sc/cmpxchg

  • 强原子访问 -> 通过覆盖 shouldInsertFencesForAtomic()emitLeadingFence()emitTrailingFence(),使用 monotonic 访问 + fence

  • atomic rmw -> 通过覆盖 expandAtomicRMWInIR(),使用带有 cmpxchg 或 load-linked/store-conditional 的循环

  • 扩展到 __atomic_* 库调用,以支持不支持的大小。

  • part-word atomicrmw/cmpxchg -> 通过覆盖 shouldExpandAtomicRMWInIRemitMaskedAtomicRMWIntrinsicshouldExpandAtomicCmpXchgInIRemitMaskedAtomicCmpXchgIntrinsic,使用特定于目标的内在函数。

有关这些示例,请查看 ARM(前五个降低)或 RISC-V(最后一个降低)后端。

AtomicExpandPass 支持两种策略,用于将 atomicrmw/cmpxchg 降低到 load-linked/store-conditional (LL/SC) 循环。第一种策略在 IR 中扩展 LL/SC 循环,调用目标降低钩子以发出 LL 和 SC 操作的内在函数。但是,许多架构对 LL/SC 循环有严格的要求,以确保向前进展,例如对循环中指令的数量和类型的限制。当循环在 LLVM IR 中扩展时,不可能强制执行这些限制,因此受影响的目标可能更喜欢在非常晚的阶段(即在寄存器分配之后)扩展到 LL/SC 循环。AtomicExpandPass 可以通过为可以在 LL/SC 循环外部执行的任何移位和掩码操作生成 IR,来帮助支持降低 part-word atomicrmw 或 cmpxchg 的这种策略。

库调用:__atomic_*

LLVM 生成两种原子库调用。请注意,这两组库函数在某种程度上令人困惑地共享 clang 定义的内建函数的名称。尽管如此,库函数与内建函数没有直接关系:__atomic_* 内建函数不会降低为 __atomic_* 库调用,__sync_* 内建函数也不会降低为 __sync_* 库调用,情况并非如此。

第一组库函数命名为 __atomic_*。此集合已由 GCC “标准化”,并在下面描述。(另请参阅 GCC 的文档

LLVM 的 AtomicExpandPass 会将数据大小超过 MaxAtomicSizeInBitsSupported 的原子操作转换为对这些函数的调用。

有四个通用函数,可以使用任何大小或对齐的数据调用

void __atomic_load(size_t size, void *ptr, void *ret, int ordering)
void __atomic_store(size_t size, void *ptr, void *val, int ordering)
void __atomic_exchange(size_t size, void *ptr, void *val, void *ret, int ordering)
bool __atomic_compare_exchange(size_t size, void *ptr, void *expected, void *desired, int success_order, int failure_order)

还有上述函数的大小专用版本,这些版本只能与适当大小的自然对齐指针一起使用。在下面的签名中,“N”是 1、2、4、8 和 16 之一,而 “iN” 是该大小的适当整数类型;如果不存在此类整数类型,则无法使用专用化

iN __atomic_load_N(iN *ptr, iN val, int ordering)
void __atomic_store_N(iN *ptr, iN val, int ordering)
iN __atomic_exchange_N(iN *ptr, iN val, int ordering)
bool __atomic_compare_exchange_N(iN *ptr, iN *expected, iN desired, int success_order, int failure_order)

最后,还有一些读-修改-写函数,这些函数仅在大小特定的变体中可用(任何其他大小都使用 __atomic_compare_exchange 循环)

iN __atomic_fetch_add_N(iN *ptr, iN val, int ordering)
iN __atomic_fetch_sub_N(iN *ptr, iN val, int ordering)
iN __atomic_fetch_and_N(iN *ptr, iN val, int ordering)
iN __atomic_fetch_or_N(iN *ptr, iN val, int ordering)
iN __atomic_fetch_xor_N(iN *ptr, iN val, int ordering)
iN __atomic_fetch_nand_N(iN *ptr, iN val, int ordering)

这组库函数有一些有趣的实现要求需要注意

  • 它们支持所有大小和对齐方式——包括那些在任何现有硬件上都无法本机实现的。因此,对于某些大小/对齐方式,它们肯定会使用互斥锁。

  • 因此,它们不能在静态链接的编译器支持库中发布,因为它们具有必须在程序中加载的所有 DSO 之间共享的状态。它们必须在所有对象使用的共享库中提供。

  • 无锁支持的原子大小集必须是任何编译器可以发出的大小的超集。也就是说:如果新的编译器引入了对大小为 N 的内联无锁原子操作的支持,则 __atomic_* 函数也必须具有大小为 N 的无锁实现。这是一项要求,以便旧编译器生成的代码(将调用 __atomic_* 函数)与新编译器生成的代码(将使用本机原子指令)互操作。

请注意,可以通过使用编译器原子内建函数本身来实现对受支持大小的自然对齐指针的操作,并使用通用互斥锁实现其他操作,从而编写完全独立于目标的这些库函数的实现。

库调用:__sync_*

某些目标或 OS/目标组合可以支持无锁原子操作,但由于各种原因,内联发出指令是不切实际的。

这里有两个典型的例子。

某些 CPU 支持可以在函数调用边界上来回切换的多个指令集。例如,MIPS 支持 MIPS16 ISA,它的指令编码比通常的 MIPS32 ISA 更小。ARM 类似地具有 Thumb ISA。在 MIPS16 和早期版本的 Thumb 中,原子指令是不可编码的。但是,这些指令可以通过函数调用调用具有更长编码的函数来获得。

此外,一些 OS/目标对提供内核支持的无锁原子操作。ARM/Linux 就是一个例子:内核 提供了一个函数,该函数在较旧的 CPU 上包含一个“神奇的可重启”原子序列(只要只有一个 CPU,它看起来就是原子的),并在较新的多核模型上包含实际的原子指令。如果所有缺少原子比较和交换支持的 CPU 都是单处理器(无 SMP),则通常可以在任何架构上提供这种功能。几乎总是这种情况。唯一没有该属性的常见架构是 SPARC – SPARCV8 SMP 系统很常见,但它不支持任何类型的比较和交换操作。

某些目标(如 RISCV)支持 +forced-atomics 目标特性,即使 LLVM 不知道任何特定的 OS 支持,也允许使用无锁原子操作。在这种情况下,用户负责确保必要的 __sync_* 实现可用。如果原子变量跨越 ABI 边界,则使用 +forced-atomics 的代码与不使用该特性的代码 ABI 不兼容。

在上述任何一种情况下,LLVM 中的 Target 都可以声明支持适当大小的原子操作,然后通过对 __sync_* 函数的库调用来实现操作的某个子集。此类函数不得在其实现中使用锁,因为与 AtomicExpandPass 使用的 __atomic_* 例程不同,这些函数可以与目标降低的原生指令混合和匹配。

此外,这些例程不需要共享,因为它们是无状态的。因此,在一个二进制文件中包含多个副本没有问题。因此,通常这些例程由静态链接的编译器运行时支持库实现。

如果目标 ISelLowering 代码已将相应的 ATOMIC_CMPXCHGATOMIC_SWAPATOMIC_LOAD_* 操作设置为 “Expand”,并且如果它通过调用 initSyncLibcalls() 选择加入这些库函数的可用性,则 LLVM 将发出对适当的 __sync_* 例程的调用。

LLVM 可以调用的完整函数集(对于 N 为 1、2、4、8 或 16)是:

iN __sync_val_compare_and_swap_N(iN *ptr, iN expected, iN desired)
iN __sync_lock_test_and_set_N(iN *ptr, iN val)
iN __sync_fetch_and_add_N(iN *ptr, iN val)
iN __sync_fetch_and_sub_N(iN *ptr, iN val)
iN __sync_fetch_and_and_N(iN *ptr, iN val)
iN __sync_fetch_and_or_N(iN *ptr, iN val)
iN __sync_fetch_and_xor_N(iN *ptr, iN val)
iN __sync_fetch_and_nand_N(iN *ptr, iN val)
iN __sync_fetch_and_max_N(iN *ptr, iN val)
iN __sync_fetch_and_umax_N(iN *ptr, iN val)
iN __sync_fetch_and_min_N(iN *ptr, iN val)
iN __sync_fetch_and_umin_N(iN *ptr, iN val)

此列表不包括任何用于原子加载或存储的函数;所有已知的架构都直接支持原子加载和存储(可能通过在正常加载或存储的任一侧发出栅栏指令)。

还有一种在某种程度上独立的可能性,可以将 ATOMIC_FENCE 降级为 __sync_synchronize()。这可能会发生,也可能不会发生,独立于上述所有内容,完全由 setOperationAction(ISD::ATOMIC_FENCE, ...) 控制。

在 AArch64 上,使用了 __sync_* 例程的变体,其中包含作为函数名一部分的内存顺序。这些例程可以在运行时确定作为 AArch64 大型系统扩展“LSE”指令集一部分引入的单指令原子操作是否可用,或者是否需要回退到 LL/SC 循环。以下辅助函数在 compiler-rtlibgcc 库中均有实现(N 是 1、2、4、8 之一,M 是 1、2、4、8 和 16 之一,ORDER 是 ‘relax’、‘acq’、‘rel’、‘acq_rel’ 之一):

iM __aarch64_casM_ORDER(iM expected, iM desired, iM *ptr)
iN __aarch64_swpN_ORDER(iN val, iN *ptr)
iN __aarch64_ldaddN_ORDER(iN val, iN *ptr)
iN __aarch64_ldclrN_ORDER(iN val, iN *ptr)
iN __aarch64_ldeorN_ORDER(iN val, iN *ptr)
iN __aarch64_ldsetN_ORDER(iN val, iN *ptr)

请注意,如果为 AArch64 目标指定了 LSE 指令集,则不会生成带外原子调用,而是使用单指令原子操作。