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 上進行最佳化 Pass 的人員提供指南,說明如何在並行情況下處理具有特殊語意的指令。這並非旨在成為精確的語意指南;細節可能會變得極其複雜且難以理解,而且通常不是必要的。
原子操作外的最佳化¶
基本的 '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) 柵欄大致等同於取得 (Acquire) 載入,而單調 (Monotonic) 儲存後跟釋放 (Release) 柵欄大致等同於釋放 (Release) 儲存。循序一致性 (SequentiallyConsistent) 柵欄的行為既像取得 (Acquire) 柵欄又像釋放 (Release) 柵欄,並且還提供總順序和一些複雜的保證,詳情請參閱 C++ 標準。
產生原子指令的前端通常需要在一定程度上了解目標;原子指令保證是無鎖的,因此寬度超出目標原生支援的指令可能無法產生。
原子順序¶
為了在效能和必要的保證之間取得平衡,有六個原子性層級。它們按強度順序排列;每個層級都包含前一個層級的所有保證,但取得/釋放 (Acquire/Release) 除外。(另請參閱 LangRef Ordering。)
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¶
Monotonic 是可用於同步原語的最弱原子性層級,儘管它不提供任何通用同步。它基本上保證,如果您採用影響特定位址的所有操作,則存在一致的順序。
- 相關標準
這對應於 C++/C
memory_order_relaxed
;請參閱這些標準以取得精確定義。- 前端注意事項
如果您正在編寫直接使用此功能的前端,請謹慎使用。就同步而言,保證非常弱,因此請確保這些僅用於您知道正確的模式中。通常,這些將用於不保護其他記憶體的原子操作(例如原子計數器),或與
fence
一起使用。- 最佳化器注意事項
就最佳化器而言,這可以視為對相關記憶體位置的讀取+寫入(並且別名分析將利用這一點)。此外,將非原子和 Unordered 載入重新排序到 Monotonic 載入周圍是合法的。允許 CSE/DSE 和一些其他最佳化,但 Monotonic 操作不太可能以使這些最佳化有用的方式使用。
- 程式碼產生注意事項
程式碼產生本質上與載入和儲存的無序程式碼產生相同。不需要柵欄。
cmpxchg
和atomicrmw
必須顯示為單個操作。
Acquire¶
Acquire 提供了取得鎖以使用常規載入和儲存存取其他記憶體所需的柵欄類型。
- 相關標準
這對應於 C++/C
memory_order_acquire
。它也應該用於 C++/Cmemory_order_consume
。- 前端注意事項
如果您正在編寫直接使用此功能的前端,請謹慎使用。Acquire 僅在與 Release 操作配對時才提供語意保證。
- 最佳化器注意事項
不了解原子操作的最佳化器可以將其視為 nothrow 呼叫。也可以將儲存從取得 (Acquire) 載入或讀取-修改-寫入操作之前移動到之後,並將非取得 (Acquire) 載入從取得 (Acquire) 操作之前移動到之後。
- 程式碼產生注意事項
具有弱記憶體排序的架構(基本上是當今除 x86 和 SPARC 之外的所有架構)需要某種柵欄來維護取得 (Acquire) 語意。所需精確的柵欄因架構而異,但對於簡單的實作,大多數架構都提供了一個足夠強大的柵欄來應對所有情況(ARM 上的
dmb
、PowerPC 上的sync
等)。在等效的 Monotonic 操作之後放置這樣的柵欄足以維護記憶體操作的取得 (Acquire) 語意。
Release¶
Release 類似於 Acquire,但具有釋放鎖所需的柵欄類型。
- 相關標準
這對應於 C++/C
memory_order_release
。- 前端注意事項
如果您正在編寫直接使用此功能的前端,請謹慎使用。Release 僅在與 Acquire 操作配對時才提供語意保證。
- 最佳化器注意事項
不了解原子操作的最佳化器可以將其視為 nothrow 呼叫。也可以將載入從釋放 (Release) 儲存或讀取-修改-寫入操作之後移動到之前,並將非釋放 (Release) 儲存從釋放 (Release) 操作之後移動到之前。
- 程式碼產生注意事項
請參閱關於 Acquire 的章節;相關操作之前的柵欄通常足以用於 Release。請注意,儲存-儲存柵欄不足以實作 Release 語意;儲存-儲存柵欄通常不會暴露給 IR,因為它們極其難以正確使用。
AcquireRelease¶
AcquireRelease(IR 中的 acq_rel
)同時提供取得 (Acquire) 和釋放 (Release) 柵欄(適用於柵欄以及同時讀取和寫入記憶體的操作)。
- 相關標準
這對應於 C++/C
memory_order_acq_rel
。- 前端注意事項
如果您正在編寫直接使用此功能的前端,請謹慎使用。Acquire 僅在與 Release 操作配對時才提供語意保證,反之亦然。
- 最佳化器注意事項
一般而言,最佳化器應將其視為 nothrow 呼叫;可能進行的最佳化通常並不有趣。
- 程式碼產生注意事項
此操作具有取得 (Acquire) 和釋放 (Release) 語意;請參閱關於 Acquire 和 Release 的章節。
SequentiallyConsistent¶
SequentiallyConsistent(IR 中的 seq_cst
)為載入提供取得 (Acquire) 語意,為儲存提供釋放 (Release) 語意。此外,它保證所有 SequentiallyConsistent 操作之間存在總順序。
- 相關標準
這對應於 C++/C
memory_order_seq_cst
、Java volatile 和不另行指定的 gcc 相容__sync_*
內建函數。- 前端注意事項
如果前端正在公開原子操作,則程式設計師更容易理解這些操作,並且使用它們通常是實際的效能權衡。
- 最佳化器注意事項
不了解原子操作的最佳化器可以將其視為 nothrow 呼叫。對於 SequentiallyConsistent 載入和儲存,允許與取得 (Acquire) 載入和釋放 (Release) 儲存相同的重新排序,但 SequentiallyConsistent 操作可能不會重新排序。
- 程式碼產生注意事項
SequentiallyConsistent 載入至少需要與取得 (Acquire) 操作相同的柵欄,而 SequentiallyConsistent 儲存需要釋放 (Release) 柵欄。此外,程式碼產生器必須強制執行 SequentiallyConsistent 儲存之後接著 SequentiallyConsistent 載入的順序。這通常透過在載入之前發出完整柵欄或在儲存之後發出完整柵欄來完成;哪種方法更可取因架構而異。
原子操作與 IR 最佳化¶
供最佳化器編寫者查詢的謂詞
isSimple()
:非 volatile 或非原子的載入或儲存。例如,memcpyopt 會檢查它可能轉換的操作。isUnordered()
:非 volatile 且最多為 Unordered 的載入或儲存。例如,LICM 在提升操作之前會檢查此項。mayReadFromMemory()
/mayWriteToMemory()
:現有謂詞,但請注意,它們對於任何 volatile 或至少為 Monotonic 的操作都會傳回 true。isStrongerThan
/isAtLeastOrStrongerThan
:這些是關於順序的謂詞。它們對於了解原子操作的 Pass 可能很有用,例如,在單個原子存取中執行 DSE,但不在釋放-取得 (release-acquire) 對中執行 DSE(有關範例,請參閱 MemoryDependencyAnalysis)別名分析:請注意,AA 將為任何 Acquire 或 Release 以及任何 Monotonic 操作存取的位址傳回 ModRef。
為了支援圍繞原子操作進行最佳化,請確保您正在使用正確的謂詞;如果這樣做,一切都應該正常運作。如果您的 Pass 應最佳化某些原子操作(尤其是 Unordered 操作),請確保它不會用非原子操作替換原子載入或儲存。
最佳化如何與各種原子操作互動的一些範例
memcpyopt
:原子操作不能最佳化為 memcpy/memset 的一部分,包括無序載入/儲存。它可以跨越某些原子操作提取操作。LICM:無序載入/儲存可以移出迴圈。它只是將 Monotonic 操作視為對記憶體位置的讀取+寫入,並將任何比這更嚴格的操作視為 nothrow 呼叫。
DSE:無序儲存可以像常規儲存一樣進行 DSE。Monotonic 儲存在某些情況下可以進行 DSE,但很難理解,而且並非特別重要。在某些情況下,DSE 可以在更強大的原子操作中運作,但這相當棘手。DSE 將此推理委託給 MemoryDependencyAnalysis(其他 Pass(如 GVN)也使用它)。
摺疊載入:來自常數全域變數的任何原子載入都可以常數摺疊,因為它無法被觀察到。類似的推理允許使用原子載入和儲存進行 sroa。
原子操作與程式碼產生¶
原子操作在 SelectionDAG 中以 ATOMIC_*
opcode 表示。在對所有原子順序使用柵欄指令的架構(如 ARM)上,如果 shouldInsertFencesForAtomic()
傳回 true,則 AtomicExpand 程式碼產生 Pass 可以發出適當的柵欄。
目前,所有原子操作的 MachineMemOperand 都標記為 volatile;這在 IR 意義上的 volatile 中是不正確的,但 CodeGen 非常保守地處理任何標記為 volatile 的內容。這應該在某個時候得到修正。
原子操作的一個非常重要的屬性是,如果您的後端支援任何給定大小的行內無鎖原子操作,則您應該以無鎖方式支援該大小的所有操作。
當目標實作原子 cmpxchg
或 LL/SC 指令時(大多數目標都這樣做),這很簡單:所有其他操作都可以在這些原語之上實作。但是,在許多較舊的 CPU(例如 ARMv5、SparcV8、Intel 80386)上,都有原子載入和儲存指令,但沒有 cmpxchg
或 LL/SC。由於使用原生指令實作 atomic load
是無效的,但使用對使用互斥鎖的函數的函式庫呼叫來實作 cmpxchg
是無效的,因此 atomic load
也必須擴展到此類架構上的函式庫呼叫,以便它可以保持原子性,以應對同時發生的 cmpxchg
,方法是使用相同的互斥鎖。
AtomicExpandPass 可以幫助解決此問題:它會將大於 setMaxAtomicSizeInBitsSupported
(預設值為 0)設定的最大大小的所有原子操作擴展為正確的 __atomic_*
函式庫呼叫。
在 x86 上,所有原子載入都會產生 MOV
。循序一致性 (SequentiallyConsistent) 儲存會產生 XCHG
,其他儲存會產生 MOV
。循序一致性 (SequentiallyConsistent) 柵欄會產生 MFENCE
,其他柵欄不會導致產生任何程式碼。cmpxchg
使用 LOCK CMPXCHG
指令。atomicrmw xchg
使用 XCHG
,atomicrmw add
和 atomicrmw sub
使用 XADD
,所有其他 atomicrmw
操作都會產生一個帶有 LOCK CMPXCHG
的迴圈。根據結果的使用者,某些 atomicrmw
操作可以轉換為類似 LOCK AND
的操作,但這通常不起作用。
在 ARM(v8 之前)、MIPS 和許多其他 RISC 架構上,取得 (Acquire)、釋放 (Release) 和循序一致性 (SequentiallyConsistent) 語意需要每個此類操作的柵欄指令。載入和儲存會產生常規指令。cmpxchg
和 atomicrmw
可以使用帶有 LL/SC 風格指令的迴圈來表示,這些指令在快取行上採用某種獨佔鎖(ARM 上的 LDREX
和 STREX
等)。
後端通常最容易使用 AtomicExpandPass 來降低某些原子建構的層級。以下是它可以執行的一些降低層級操作
cmpxchg -> 帶有 load-linked/store-conditional 的迴圈,方法是覆寫
shouldExpandAtomicCmpXchgInIR()
、emitLoadLinked()
、emitStoreConditional()
大型載入/儲存 -> ll-sc/cmpxchg,方法是覆寫
shouldExpandAtomicStoreInIR()
/shouldExpandAtomicLoadInIR()
強原子存取 -> 單調存取 + 柵欄,方法是覆寫
shouldInsertFencesForAtomic()
、emitLeadingFence()
和emitTrailingFence()
atomic rmw -> 帶有 cmpxchg 或 load-linked/store-conditional 的迴圈,方法是覆寫
expandAtomicRMWInIR()
擴展到不受支援大小的 __atomic_* 函式庫呼叫。
部分字組 atomicrmw/cmpxchg -> 目標特定的內建函數,方法是覆寫
shouldExpandAtomicRMWInIR
、emitMaskedAtomicRMWIntrinsic
、shouldExpandAtomicCmpXchgInIR
和emitMaskedAtomicCmpXchgIntrinsic
。
如需這些範例,請查看 ARM(前五個降低層級)或 RISC-V(最後一個降低層級)後端。
AtomicExpandPass 支援兩種策略,用於將 atomicrmw/cmpxchg 降低為 load-linked/store-conditional (LL/SC) 迴圈。第一種策略在 IR 中擴展 LL/SC 迴圈,呼叫目標降低層級 Hook 以發出 LL 和 SC 操作的內建函數。但是,許多架構對 LL/SC 迴圈有嚴格的要求,以確保向前進展,例如對迴圈中指令的數量和類型的限制。當迴圈在 LLVM IR 中擴展時,不可能強制執行這些限制,因此受影響的目標可能更喜歡在非常後期的階段(即在暫存器分配之後)擴展到 LL/SC 迴圈。AtomicExpandPass 可以透過產生可在 LL/SC 迴圈之外執行的任何移位和遮罩的 IR,來幫助支援降低部分字組 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_*¶
某些目標或作業系統/目標組合可以支援無鎖原子操作,但由於各種原因,內聯發出指令並不實際。
這裡有兩個典型的範例。
某些 CPU 支援多個指令集,可以在函數呼叫邊界來回切換。例如,MIPS 支援 MIPS16 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
的程式碼與不使用該特性的程式碼在 ABI 上是不相容的。
在上述任一種情況下,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 指令集,則不會產生行外原子操作呼叫,而是使用單指令原子操作來代替。