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 atomic
和 store atomic
提供与非原子加载和存储相同的基本功能,但在涉及线程和信号的情况中提供额外的保证。
cmpxchg
和 atomicrmw
本质上类似于原子加载后跟原子存储(对于 cmpxchg
,存储是有条件的),但在任何线程上,加载和存储之间都不能发生其他内存操作。
一个 fence
提供不是其他操作一部分的 Acquire 和/或 Release 排序;它通常与 Monotonic 内存操作一起使用。Monotonic 加载后跟 Acquire fence 大致等效于 Acquire 加载,而 Monotonic 存储后跟 Release fence 大致等效于 Release 存储。SequentiallyConsistent fence 既充当 Acquire fence 又充当 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 调用。但是,重新排序无序操作是安全的,优化器应利用这一点,因为无序操作在需要它们的语言中很常见。
- 代码生成说明
这些操作必须是原子的,从某种意义上说,如果您使用无序加载和无序存储,则加载不能看到从未存储的值。正常的加载或存储指令通常就足够了,但请注意,无序加载或存储不能拆分为多个指令(或执行多个内存操作的指令,例如 ARM 上没有 LPAE 的
LDRD
,或 LPAE ARM 上的非自然对齐LDRD
)。
Monotonic¶
单调性是同步原语中可以使用最弱的原子性级别,尽管它不提供任何常规的同步。它本质上保证了如果您获取影响特定地址的所有操作,则存在一致的顺序。
- 相关标准
这对应于 C++/C 的
memory_order_relaxed
;有关确切定义,请参阅这些标准。- 前端说明
如果您正在编写直接使用此功能的前端,请谨慎使用。在同步方面,其保证非常弱,因此请确保仅在您知道正确的模式下使用它们。通常,这些要么用于不保护其他内存的原子操作(如原子计数器),要么与
fence
结合使用。- 优化器说明
在优化器方面,这可以被视为对相关内存位置的读+写操作(别名分析将利用这一点)。此外,允许在单调加载周围重新排序非原子和无序加载。允许使用 CSE/DSE 和一些其他优化,但单调操作不太可能以使这些优化有用的方式使用。
- 代码生成说明
代码生成本质上与加载和存储的无序代码生成相同。不需要栅栏。
cmpxchg
和atomicrmw
必须显示为单个操作。
获取¶
获取提供了一种必要的屏障,用于获取锁以使用正常的加载和存储访问其他内存。
- 相关标准
这对应于 C++/C 的
memory_order_acquire
。它也应该用于 C++/C 的memory_order_consume
。- 前端说明
如果您正在编写直接使用此功能的前端,请谨慎使用。仅当与释放操作配对时,获取才提供语义保证。
- 优化器说明
不了解原子的优化器可以将其视为一个无异常调用。还可以将存储从获取加载或读-修改-写操作之前移动到之后,并将非获取加载从获取操作之前移动到之后。
- 代码生成说明
具有弱内存排序的体系结构(本质上是当今除 x86 和 SPARC 之外的所有相关体系结构)需要某种类型的栅栏来维护获取语义。所需的精确栅栏因体系结构而异,但对于简单的实现,大多数体系结构都提供了一个足够强大的屏障(ARM 上的
dmb
,PowerPC 上的sync
等)。在等效的单调操作之后放置这样的栅栏足以维护内存操作的获取语义。
释放¶
释放类似于获取,但具有释放锁所需的屏障。
- 相关标准
这对应于 C++/C 的
memory_order_release
。- 前端说明
如果您正在编写直接使用此功能的前端,请谨慎使用。仅当与获取操作配对时,释放才提供语义保证。
- 优化器说明
不了解原子的优化器可以将其视为一个无异常调用。还可以将加载从释放存储或读-修改-写操作之后移动到之前,并将非释放存储从释放操作之后移动到之前。
- 代码生成说明
请参阅获取部分;相关操作之前的栅栏通常足以用于释放。请注意,存储-存储栅栏不足以实现释放语义;存储-存储栅栏通常不会公开给 IR,因为它们非常难以正确使用。
获取释放¶
获取释放(IR 中的 acq_rel
)同时提供获取和释放屏障(对于栅栏和读写内存的操作)。
- 相关标准
这对应于 C++/C 的
memory_order_acq_rel
。- 前端说明
如果您正在编写直接使用此功能的前端,请谨慎使用。获取仅在与释放操作配对时才提供语义保证,反之亦然。
- 优化器说明
通常,优化器应将其视为无异常调用;可能的优化通常并不重要。
- 代码生成说明
此操作具有获取和释放语义;请参阅获取和释放部分。
顺序一致¶
顺序一致(IR 中的 seq_cst
)为加载提供获取语义,为存储提供释放语义。此外,它保证所有顺序一致操作之间存在一个总排序。
- 相关标准
这对应于 C++/C 的
memory_order_seq_cst
、Java volatile 和不另行指定的 gcc 兼容__sync_*
内建函数。- 前端说明
如果前端正在公开原子操作,那么对于程序员来说,这些操作比其他类型的操作更容易推理,并且使用它们通常是实用的性能权衡。
- 优化器说明
不了解原子的优化器可以将其视为一个无异常调用。对于顺序一致的加载和存储,允许相同的重新排序,就像获取加载和释放存储一样,除了顺序一致的操作不能重新排序。
- 代码生成说明
顺序一致的加载至少需要与获取操作相同的屏障,而顺序一致的存储需要释放屏障。此外,代码生成器必须在顺序一致的存储后跟顺序一致的加载之间强制执行排序。这通常通过在加载之前发出完整栅栏或在存储之后发出完整栅栏来完成;哪个更可取取决于体系结构。
原子和 IR 优化¶
优化器编写器查询的谓词
isSimple()
:不是易变的或原子的加载或存储。例如,memcpyopt 会检查它可能转换的操作。isUnordered()
:不是易变的并且最多是无序的加载或存储。例如,LICM 在提升操作之前会检查它。mayReadFromMemory()
/mayWriteToMemory()
:现有谓词,但请注意,它们对任何易变或至少是单调的操作都返回 true。isStrongerThan
/isAtLeastOrStrongerThan
:这些是排序上的谓词。它们对于了解原子的传递很有用,例如在单个原子访问中执行 DSE,但不能跨释放-获取对执行(请参阅 MemoryDependencyAnalysis 以了解此示例)别名分析:请注意,AA 将对任何获取或释放以及任何单调操作访问的地址返回 ModRef。
为了支持围绕原子操作进行优化,请确保您正在使用正确的谓词;如果这样做,一切都会正常工作。如果您的传递应该优化某些原子操作(尤其是无序操作),请确保它不会用非原子操作替换原子加载或存储。
优化如何与各种原子操作交互的一些示例
memcpyopt
:原子操作不能被优化为 memcpy/memset 的一部分,包括无序加载/存储。它可以跨某些原子操作提取操作。LICM:无序加载/存储可以从循环中移出。它只是将单调操作视为对内存位置的读+写,并将比这更严格的任何操作视为无异常调用。
DSE:无序存储可以像普通存储一样进行 DSE。单调存储在某些情况下可以进行 DSE,但推理起来很棘手,而且并不特别重要。在某些情况下,DSE 可以在更强的原子操作中进行操作,但这相当棘手。DSE 将此推理委托给 MemoryDependencyAnalysis(GVN 等其他传递也使用它)。
折叠加载:来自常量全局的任何原子加载都可以进行常量折叠,因为它无法被观察到。类似的推理允许使用原子加载和存储进行 sroa。
原子和代码生成¶
原子操作在 SelectionDAG 中以 ATOMIC_*
操作码表示。在对所有原子排序使用屏障指令的体系结构(如 ARM)上,如果 shouldInsertFencesForAtomic()
返回 true,则 AtomicExpand 代码生成传递可以发出适当的栅栏。
所有原子操作的 MachineMemOperand 目前都被标记为易变的;这在 IR 易变的意义上是不正确的,但 CodeGen 对任何标记为易变的事物都非常保守地处理。这应该在某个时候得到修复。
原子操作的一个非常重要的属性是,如果您的后端支持任何给定大小的内联无锁原子操作,则您应该以无锁方式支持该大小的所有操作。
当目标实现原子 cmpxchg
或 LL/SC 指令(大多数都这样做)时,这很简单:所有其他操作都可以在这些原语之上实现。但是,在许多较旧的 CPU(例如 ARMv5、SparcV8、Intel 80386)上,存在原子加载和存储指令,但没有 cmpxchg
或 LL/SC。由于使用本机指令实现 atomic load
是无效的,但使用库调用到使用互斥锁的函数的 cmpxchg
是有效的,因此 atomic load
也必须扩展到此类体系结构上的库调用,以便它可以保持原子关于同时的 cmpxchg
,通过使用相同的互斥锁。
AtomicExpandPass 可以帮助解决这个问题:它将所有原子操作扩展到适当的 __atomic_*
libcalls,用于大于由 setMaxAtomicSizeInBitsSupported
设置的最大值的任何大小(默认为 0)。
在 x86 上,所有原子加载都会生成一个 MOV
指令。顺序一致性存储会生成一个 XCHG
指令,其他存储会生成一个 MOV
指令。顺序一致性栅栏会生成一个 MFENCE
指令,其他栅栏不会生成任何代码。 cmpxchg
使用 LOCK CMPXCHG
指令。 atomicrmw xchg
使用 XCHG
指令,atomicrmw add
和 atomicrmw sub
使用 XADD
指令,所有其他 atomicrmw
操作都会生成一个包含 LOCK CMPXCHG
指令的循环。根据结果的使用方式,一些 atomicrmw
操作可以转换为诸如 LOCK AND
之类的操作,但这并不总是有效。
在 ARM(v8 之前)、MIPS 和许多其他 RISC 架构上,获取、释放和顺序一致性语义需要为每个此类操作使用屏障指令。加载和存储会生成正常的指令。 cmpxchg
和 atomicrmw
可以使用包含 LL/SC 样式指令的循环来表示,这些指令会在缓存行上获取某种排他锁(ARM 上的 LDREX
和 STREX
等)。
后端通常最容易使用 AtomicExpandPass 来降低一些原子构造。以下是一些它可以执行的降低操作
cmpxchg -> 通过重写
shouldExpandAtomicCmpXchgInIR()
、emitLoadLinked()
、emitStoreConditional()
来转换为包含 load-linked/store-conditional 的循环大型加载/存储 -> 通过重写
shouldExpandAtomicStoreInIR()
/shouldExpandAtomicLoadInIR()
来转换为 ll-sc/cmpxchg强原子访问 -> 通过重写
shouldInsertFencesForAtomic()
、emitLeadingFence()
和emitTrailingFence()
来转换为单调访问 + 栅栏原子 rmw -> 通过重写
expandAtomicRMWInIR()
来转换为包含 cmpxchg 或 load-linked/store-conditional 的循环对于不支持的大小,扩展到 __atomic_* 库函数。
部分字原子rmw/cmpxchg -> 通过重写
shouldExpandAtomicRMWInIR
、emitMaskedAtomicRMWIntrinsic
、shouldExpandAtomicCmpXchgInIR
和emitMaskedAtomicCmpXchgIntrinsic
来转换为目标特定的内联函数。
有关这些示例,请查看 ARM(前五个降低操作)或 RISC-V(最后一个降低操作)后端。
AtomicExpandPass 支持两种将原子rmw/cmpxchg 降低到 load-linked/store-conditional (LL/SC) 循环的策略。第一种在 IR 中扩展 LL/SC 循环,并调用目标降低钩子来为 LL 和 SC 操作发出内联函数。但是,许多架构对 LL/SC 循环有严格的要求,以确保向前进度,例如对循环中指令的数量和类型的限制。当在 LLVM IR 中扩展循环时,无法执行这些限制,因此受影响的目标可能更喜欢在非常晚的阶段(即寄存器分配之后)扩展到 LL/SC 循环。AtomicExpandPass 可以通过生成可以在 LL/SC 循环之外执行的任何移位和掩码的 IR,来帮助支持使用此策略降低部分字原子rmw 或 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_*¶
某些目标或操作系统/目标组合可以支持无锁原子操作,但由于各种原因,内联发出指令并不实用。
这有两个典型的例子。
某些 CPU 支持多个指令集,这些指令集可以在函数调用边界上相互切换。例如,MIPS 支持 MIPS16 ISA,该 ISA 具有比通常的 MIPS32 ISA 更小的指令编码。类似地,ARM 也具有 Thumb ISA。在 MIPS16 和早期版本的 Thumb 中,原子指令无法编码。但是,这些指令可以通过对具有较长编码的函数的函数调用来获得。
此外,一些操作系统/目标对提供了内核支持的无锁原子操作。ARM/Linux 就是一个例子:内核 提供了一个函数,该函数在较旧的 CPU 上包含一个“可神奇重启”的原子序列(只要只有一个 CPU,它看起来就是原子的),并在较新的多核型号上包含实际的原子指令。如果所有缺少原子比较和交换支持的 CPU 都是单处理器(没有 SMP),则可以在任何架构上提供这种功能。这几乎总是这样。唯一没有此属性的通用架构是 SPARC - SPARCV8 SMP 系统很常见,但它不支持任何类型的比较和交换操作。
某些目标(如 RISCV)支持 +forced-atomics
目标特性,即使 LLVM 不知道任何特定操作系统对它们的任何支持,它也会启用无锁原子的使用。在这种情况下,用户负责确保必要的 __sync_*
实现可用。如果原子变量跨越 ABI 边界,则使用 +forced-atomics
的代码与不使用该特性的代码不兼容。
在这两种情况下,LLVM 中的目标都可以声明对适当大小的原子操作的支持,然后通过对 __sync_*
函数的库函数调用来实现某些操作子集。此类函数必须在其实现中不使用锁,因为与 AtomicExpandPass 使用的 __atomic_*
例程不同,这些函数可能会与目标降低发出的本机指令混合和匹配。
此外,这些例程不需要共享,因为它们是无状态的。因此,在同一个二进制文件中包含多个副本没有问题。因此,这些例程通常由静态链接的编译器运行时支持库实现。
如果目标 ISelLowering 代码已将相应的 ATOMIC_CMPXCHG
、ATOMIC_SWAP
或 ATOMIC_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-rt
和 libgcc
库中实现(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 指令集,则不会生成行外原子调用,并且会使用单指令原子操作来代替。