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'
允許各種優化,但在並行環境中可能會導致未定義的結果;請參閱非原子性。本節將專門討論在並行環境中應用的一項優化器限制,並對其進行更詳細的說明,因為任何處理存放的優化都需要意識到它。
從優化器的角度來看,規則是,如果沒有涉及原子排序的指令,則並行性無關緊要,但有一種例外情況:如果變數可能對另一個執行緒或信號處理常式可見,則不能沿著可能無法執行的路徑插入存放。以下面的例子為例:
/* 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
提供不屬於另一個操作的獲取和/或釋放排序;它通常與單調記憶體操作一起使用。後跟獲取柵欄的單調載入大致等效於獲取載入,後跟釋放柵欄的單調存放大致等效於釋放存放。順序一致性柵欄的行為既像獲取柵欄又像釋放柵欄,並且還提供具有某些複雜保證的總排序,有關詳細資訊,請參閱 C++ 標準。
產生原子指令的前端通常需要在某種程度上瞭解目標;保證原子指令是無鎖的,因此,產生比目標原生支援的指令更寬的指令是不可能的。
原子排序¶
為了在效能和必要保證之間取得平衡,共有六個原子性級別。它們按強度順序列出;每個級別都包含先前級別的所有保證,但獲取/釋放除外。(另請參閱LangRef 排序。)
非原子性¶
非原子性很明顯,它是非原子的載入或存放。(這實際上不是原子性級別,而是為了便於比較而在此處列出。)這本質上是常規的載入或存放。如果在給定的記憶體位置上存在競爭條件,則從該位置載入會返回 undef。
- 相關標準
這旨在匹配 C/C++ 中的共享變數,並在任何其他需要內存訪問且不可能出現競爭條件的上下文中使用。(精確定義請參見語言參考手冊內存模型。)
- 前端注意事項
規則基本上是所有透過基本加載和存儲由多個線程訪問的內存都應該由鎖或其他同步機制保護;否則,您可能會遇到未定義的行為。如果您的前端是針對像 Java 這樣的“安全”語言,請使用 Unordered 來加載和存儲任何共享變數。請注意,NotAtomic volatile 加載和存儲不是正確的原子操作;不要嘗試將它們用作替代品。(根據 C/C++ 標準,volatile 確實提供了一些關於異步信號的有限保證,但原子操作通常是更好的解決方案。)
- 優化器注意事項
允許在原本不存在加載操作的代碼路徑上引入對共享變數的加載操作;不允許引入對共享變數的存儲操作。請參見原子操作之外的優化。
- 代碼生成注意事項
這裡一個有趣的限制是不允許寫入與存儲相關的字節之外的字節。這主要與未對齊的存儲有關:通常不允許將未對齊的存儲轉換為兩個與未對齊存儲寬度相同的對齊存儲。後端還應該生成一個 i8 存儲作為一個 i8 存儲,而不是一個寫入周圍字節的指令。(如果您正在為無法滿足這些限制且關心並發性的架構編寫後端,請發送電子郵件至 llvm-dev。)
無序(Unordered)¶
無序是最低級別的原子性。它基本上保證競爭條件會產生一些合理的結果,而不是未定義的行為。它還保證操作是無鎖的,因此它不依賴於數據是特殊原子結構的一部分,也不依賴於單獨的每個進程全局鎖。請注意,對於不支持的原子操作,代碼生成將會失敗;如果您需要這樣的操作,請使用顯式鎖定。
- 相關標準
這旨在匹配 Java 內存模型中共享變數的行為。
- 前端注意事項
這不能用於同步,但對於需要保證生成的代碼永遠不會表現出未定義行為的 Java 和其他“安全”語言很有用。請注意,這種保證在常見平台上對於原生寬度的加載操作來說成本很低,但對於更寬的加載操作(例如在 ARM 上的 64 位存儲)來說,成本可能很高或不可用。(Java 或其他“安全”語言的前端通常會將 ARM 上的 64 位存儲拆分為兩個 32 位無序存儲。)
- 優化器注意事項
就優化器而言,這禁止任何將單個加載轉換為多個加載、將存儲轉換為多個存儲、縮小存儲或存儲原本不會存儲的值的轉換。一些不安全優化的例子包括將賦值縮小為位字段、重新具現化加載以及將加載和存儲轉換為 memcpy 調用。但是,對無序操作重新排序是安全的,並且優化器應該利用這一點,因為無序操作在需要它們的語言中很常見。
- 代碼生成注意事項
這些操作需要是原子性的,因為如果您使用無序加載和無序存儲,則加載操作無法看到從未存儲過的值。普通的加載或存儲指令通常就足夠了,但請注意,無序加載或存儲不能拆分為多個指令(或執行多個內存操作的指令,例如沒有 LPAE 的 ARM 上的
LDRD
,或 LPAE ARM 上非自然對齊的LDRD
)。
單調(Monotonic)¶
單調性 (Monotonic) 是同步基元中可使用的最弱原子性級別,儘管它不提供任何通用的同步。它基本上保證,如果您採用影響特定地址的所有操作,則會存在一致的順序。
- 相關標準
這對應於 C++/C 的
memory_order_relaxed
;有關確切定義,請參閱這些標準。- 前端注意事項
如果您正在編寫直接使用它的前端,請謹慎使用。同步方面的保證非常薄弱,因此請確保僅在您知道正確的模式中使用它們。通常,這些操作要么用於不保護其他內存的原子操作(如原子計數器),要么與
fence
一起使用。- 優化器注意事項
就優化器而言,這可以被視為對相關內存位置的讀取+寫入(別名分析將利用這一點)。此外,允許在單調加載前後重新排序非原子和無序加載。允許使用 CSE/DSE 和其他一些優化,但單調操作不太可能以使這些優化有用的方式使用。
- 代碼生成注意事項
代碼生成與加載和存儲的無序代碼生成基本相同。不需要柵欄 (fence)。
cmpxchg
和atomicrmw
需要表現為單個操作。
獲取 (Acquire)¶
獲取提供了一種屏障,用於獲取鎖定以使用普通的加載和存儲來訪問其他內存。
- 相關標準
這對應於 C++/C 的
memory_order_acquire
。它也應該用於 C++/C 的memory_order_consume
。- 前端注意事項
如果您正在編寫直接使用它的前端,請謹慎使用。獲取僅在與釋放操作配對時才提供語義保證。
- 優化器注意事項
不知道原子的優化器可以將其視為無拋出調用。也可以將存儲從獲取加載或讀取-修改-寫入操作之前移動到之後,并将非獲取加載從獲取操作之前移動到之後。
- 代碼生成注意事項
具有弱內存排序的架構(基本上是當今所有相關的架構,除了 x86 和 SPARC)都需要某種柵欄來維持獲取語義。所需的精確柵欄因架構而異,但對於簡單的實現,大多數架構都提供了一個足夠強大的屏障(ARM 上的
dmb
,PowerPC 上的sync
等)。在等效的單調操作之後放置這樣的柵欄足以維持內存操作的獲取語義。
釋放 (Release)¶
釋放類似於獲取,但具有一種用於釋放鎖定的屏障。
- 相關標準
這對應於 C++/C 的
memory_order_release
。- 前端注意事項
如果您正在編寫直接使用它的前端,請謹慎使用。釋放僅在與獲取操作配對時才提供語義保證。
- 優化器注意事項
不知道原子的優化器可以將其視為無拋出調用。也可以將加載從釋放存儲或讀取-修改-寫入操作之後移動到之前,并将非釋放存儲從釋放操作之後移動到之前。
- 代碼生成注意事項
請參閱有關獲取的部分;相關操作之前的柵欄通常足以滿足釋放的需要。請注意,存儲-存儲柵欄不足以實現釋放語義;存儲-存儲柵欄通常不會暴露給 IR,因為它們極難正確使用。
獲取釋放 (AcquireRelease)¶
AcquireRelease (IR 中的 acq_rel
) 同時提供了 Acquire 和 Release 障礙(適用於同時讀取和寫入內存的圍欄和操作)。
- 相關標準
這對應於 C++/C 中的
memory_order_acq_rel
。- 前端注意事項
如果您正在編寫直接使用此功能的前端,請謹慎使用。Acquire 僅在與 Release 操作配對時才提供語義保證,反之亦然。
- 優化器注意事項
一般而言,最佳化器應將其視為無拋出調用;可能的最佳化通常並不重要。
- 代碼生成注意事項
此操作具有 Acquire 和 Release 語義;請參閱 Acquire 和 Release 的章節。
順序一致性¶
順序一致性 (IR 中的 seq_cst
) 為加載提供 Acquire 語義,為儲存提供 Release 語義。此外,它保證所有順序一致性操作之間存在總排序。
- 相關標準
這對應於 C++/C 中的
memory_order_seq_cst
、Java volatile 以及未指定其他行為的 gcc 相容__sync_*
內建函數。- 前端注意事項
如果前端公開原子操作,則與其他類型的操作相比,這些操作對程式設計師來說更容易理解,並且使用它們通常是一種實際的效能權衡。
- 優化器注意事項
不知道原子性的最佳化器可以將其視為無拋出調用。對於順序一致性加載和儲存,允許與 Acquire 加載和 Release 儲存相同的重新排序,但順序一致性操作可能不會重新排序。
- 代碼生成注意事項
順序一致性加載至少需要與 Acquire 操作相同的障礙,而順序一致性儲存需要 Release 障礙。此外,代碼生成器必須強制執行順序一致性儲存後接順序一致性加載之間的排序。這通常是透過在加載之前發出完整圍欄或在儲存之後發出完整圍欄來完成的;哪種方式更佳取決於架構。
原子性和 IR 最佳化¶
供最佳化器編寫者查詢的謂詞
isSimple()
:非易失性或原子性的加載或儲存。例如,這就是 memcpyopt 檢查其可能轉換的操作時所尋找的。isUnordered()
:非易失性且最多為無序的加載或儲存。例如,LICM 會在提升操作之前檢查這一點。mayReadFromMemory()
/mayWriteToMemory()
:現有謂詞,但請注意,它們對於任何易失性或至少為單調的操作都會返回 true。isStrongerThan
/isAtLeastOrStrongerThan
:這些是排序上的謂詞。它們對於瞭解原子性的過程很有用,例如,對單個原子訪問執行 DSE,但不能跨越釋放-獲取對(有關此示例,請參閱 MemoryDependencyAnalysis)別名分析:請注意,AA 將為任何 Acquire 或 Release 操作以及任何單調操作訪問的地址返回 ModRef。
為了支援圍繞原子操作進行最佳化,請確保您使用的是正確的謂詞;如果這樣做,一切應該都能正常工作。如果您的過程應該最佳化某些原子操作(特別是無序操作),請確保它不會用非原子操作替換原子加載或儲存。
最佳化如何與各種原子操作交互作用的一些示例
memcpyopt
:原子操作不能被優化成 memcpy/memset 的一部分,包括無序加載/存儲。它可以跨越某些原子操作來拉取操作。LICM:無序加載/存儲可以移出循環。它只是將單調操作視為對內存位置的讀取+寫入,而任何比這更嚴格的操作都視為無拋出調用。
DSE:無序存儲可以像普通存儲一樣進行 DSE。單調存儲在某些情況下可以進行 DSE,但推理起來很棘手,而且不是特別重要。在某些情況下,DSE 可以跨越更強的原子操作進行操作,但這相當棘手。DSE 將此推理委託給 MemoryDependencyAnalysis(其他遍歷(如 GVN)也會使用它)。
摺疊加載:任何從常量全局變量的原子加載都可以進行常量摺疊,因為它無法被觀察到。類似的推理允許使用原子加載和存儲進行 sroa。
原子操作和代碼生成¶
原子操作在 SelectionDAG 中使用 ATOMIC_*
操作碼表示。在使用屏障指令進行所有原子排序的架構(如 ARM)上,如果 shouldInsertFencesForAtomic()
返回 true,則 AtomicExpand Codegen 遍歷可以發出適當的柵欄。
目前,所有原子操作的 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
指令。依序一致性 fences 會產生 MFENCE
指令,其他 fences 則不會產生任何程式碼。cmpxchg
使用 LOCK CMPXCHG
指令。atomicrmw xchg
使用 XCHG
指令,atomicrmw add
和 atomicrmw sub
使用 XADD
指令,而所有其他 atomicrmw
操作都會產生一個包含 LOCK CMPXCHG
指令的迴圈。根據結果的使用者,某些 atomicrmw
操作可以轉換為 LOCK AND
之類的操作,但這並非在所有情況下都適用。
在 ARM(v8 之前)、MIPS 和許多其他 RISC 架構上,Acquire、Release 和 SequentiallyConsistent 語義需要對每個此類操作使用 barrier 指令。載入和存放會產生一般的指令。cmpxchg
和 atomicrmw
可以使用包含 LL/SC 類型指令的迴圈來表示,這些指令會對快取行採取某種獨佔鎖定(例如,ARM 上的 LDREX
和 STREX
)。
後端通常最容易使用 AtomicExpandPass 來降低某些原子構造。以下是一些可以降低的構造:
cmpxchg -> 透過覆寫
shouldExpandAtomicCmpXchgInIR()
、emitLoadLinked()
、emitStoreConditional()
,將其轉換為包含 load-linked/store-conditional 的迴圈。大型載入/存放 -> 透過覆寫
shouldExpandAtomicStoreInIR()
/shouldExpandAtomicLoadInIR()
,將其轉換為 ll-sc/cmpxchg。強原子存取 -> 透過覆寫
shouldInsertFencesForAtomic()
、emitLeadingFence()
和emitTrailingFence()
,將其轉換為單調存取 + fences。原子 rmw -> 透過覆寫
expandAtomicRMWInIR()
,將其轉換為包含 cmpxchg 或 load-linked/store-conditional 的迴圈。針對不支援的大小,將其擴展為 __atomic_* 函式庫呼叫。
部分字組 atomicrmw/cmpxchg -> 透過覆寫
shouldExpandAtomicRMWInIR
、emitMaskedAtomicRMWIntrinsic
、shouldExpandAtomicCmpXchgInIR
和emitMaskedAtomicCmpXchgIntrinsic
,將其轉換為目標特定的內建函式。
如需這些範例,請參閱 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,來協助使用此策略降級部分字組的 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_*
函式)產生的程式碼可以與新版編譯器(將會使用原生原子指令)產生的程式碼互通。
請注意,可以透過使用編譯器原子內建函式來實作自然對齊指標上支援大小的操作,以及其他情況下使用通用的 mutex 實作,來編寫這些函式庫函式的完全獨立於目標的實作。
Libcalls:__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_*
函式的 libcall 來實作某些操作的子集。這些函式在其實作中*不得*使用鎖,因為與 AtomicExpandPass 使用的 __atomic_*
常式不同,這些函式可以與目標降低的原生指令混合和匹配。
此外,這些常式不需要被共用,因為它們是無狀態的。因此,在一個二進制檔案中包含多個副本沒有問題。因此,通常這些常式由靜態連結的編譯器執行時支援程式庫實作。
如果目標 ISelLowering 程式碼已將相應的 ATOMIC_CMPXCHG
、ATOMIC_SWAP
或 ATOMIC_LOAD_*
操作設定為「展開」,並且它已透過呼叫 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 指令集,則不會產生行外原子呼叫,而是使用單指令原子操作。