簡體   English   中英

x86 上的原子性

[英]Atomicity on x86

8.1.2 總線鎖定

Intel 64 和 IA-32 處理器提供 LOCK# 信號,在某些關鍵內存操作期間自動斷言以鎖定系統總線或等效鏈接。 當這個輸出信號被斷言時,來自其他處理器或總線代理的總線控制請求被阻止。 軟件可以指定其他情況,當 LOCK 語義之后是在指令前加上 LOCK 前綴。

它來自英特爾手冊,第 3 卷

聽起來像內存上的原子操作將直接在內存(RAM)上執行。 我很困惑,因為我在分析匯編輸出時看到“沒什么特別的”。 基本上,為std::atomic<int> X; X.load()生成的匯編輸出std::atomic<int> X; X.load() std::atomic<int> X; X.load()只放置“額外”的 mfence。 但是,它負責正確的內存排序,而不是原子性。 如果我理解正確, X.store(2)就是mov [somewhere], $2 僅此而已。 它似乎沒有“跳過”緩存。 我知道將對齊(例如整數)移動到內存是原子的。 但是,我很困惑。


所以,我提出了我的疑慮,但主要問題是:

CPU內部是如何實現原子操作的?

聽起來像內存上的原子操作將直接在內存(RAM)上執行。

不,只要系統中每個可能的觀察者都將操作視為原子,該操作就可以只涉及緩存。

對於原子讀-修改-寫操作(例如lock add [mem], eax ,尤其是對於未對齊的地址),滿足此要求要困難得多,此時 CPU 可能會斷言 LOCK# 信號。 您仍然不會在 asm 中看到更多內容:硬件為lock ed 指令實現了 ISA 所需的語義。

雖然我懷疑現代 CPU 上是否有物理外部 LOCK# 引腳,其中內存控制器內置於 CPU 中,而不是單獨的北橋芯片中


std::atomic<int> X; X.load() std::atomic<int> X; X.load()只放置“額外”的 mfence。

編譯器不會為 seq_cst 加載 MFENCE。

我想我曾經讀到過舊的 MSVC 確實為此發出了 MFENCE(也許是為了防止使用無圍欄的 NT 商店重新排序?或者而不是在商店中?)。 但它不再是:我測試了 MSVC 19.00.23026.0。 這個程序的 asm 輸出中查找 foo 和 bar,該程序將自己的 asm 轉儲到在線編譯和運行站點中

我們在這里不需要圍欄的原因是 x86 內存模型不允許LoadStore 和 LoadLoad重新排序。 較早的(非 seq_cst)存儲仍然可以延遲到 seq_cst 加載之后,因此它與使用獨立的std::atomic_thread_fence(mo_seq_cst); X.load(mo_acquire);

如果我理解正確, X.store(2)就是mov [somewhere], 2

這與您認為加載需要mfence想法一致; 一個或另一個 seq_cst 加載或存儲需要一個完整的屏障來防止禁止StoreLoad 重新排序,否則可能會發生

在實踐中,編譯器開發人員選擇了廉價加載 (mov) / 昂貴存儲 (mov+mfence),因為加載更為常見。 C++11 到處理器的映射

(x86 內存排序模型是程序順序加上帶存儲轉發的存儲緩沖區( 另請參見)。這使得 asm 中的mo_acquiremo_release自由,只需要阻止編譯時重新排序,讓我們選擇是否放置 MFENCE裝載或存儲的完全屏障。)

所以 seq_cst 商店要么是mov + mfence要么是xchg 為什么具有順序一致性的 std::atomic 存儲使用 XCHG? 討論 xchg 在某些 CPU 上的性能優勢。 在 AMD 上,MFENCE 被 (IIRC) 記錄為具有阻止亂序執行的額外序列化管道語義(用於指令執行,而不僅僅是內存排序),並且在實踐中的某些 Intel CPU(Skylake)上,這也是案例。

MSVC 的 store 的 asm 和clang 的一樣,使用xchg用同樣的指令做 store + memory barrier。

原子發布或寬松存儲可以只是mov ,它們之間的區別僅在於允許多少編譯時重新排序。


這個問題看起來像你早期的C++ 內存模型的第 2 部分:順序一致性和原子性,你在那里問:

CPU內部是如何實現原子操作的?

正如您在問題中指出的那樣,原子性與任何其他操作的排序無關。 (即memory_order_relaxed )。 它只是意味着該操作作為單個不可分割的操作發生,因此得名,而不是作為多個部分發生,部分發生在其他事情之前,部分發生在其他事情之后。

您可以“免費”獲得原子性,無需額外的硬件來對齊加載或存儲高達內核、內存和 I/O 總線(如 PCIe)之間的數據路徑大小。 即在不同級別的緩存之間,以及在不同內核的緩存之間。 在現代設計中,內存控制器是 CPU 的一部分,因此即使是 PCIe 設備訪問內存也必須通過 CPU 的系統代理。 (這甚至讓 Skylake 的 eDRAM L4(在任何台式機 CPU 中都不可用 :()作為內存端緩存工作(與 Broadwell 不同,后者將其用作 L3 IIRC 的受害者緩存),位於內存和系統中的其他所有內容之間,因此它甚至可以緩存 DMA)。

Skylake 系統代理圖,來自 IDF 通過 ARStechnica

這意味着 CPU 硬件可以做任何必要的事情來確保存儲或加載相對於系統中可以觀察它的任何其他東西是原子的。 如果有的話,這可能並不多。 DDR 內存使用足夠寬的數據總線,因此 64 位對齊存儲確實在同一周期內通過內存總線以電氣方式傳輸到 DRAM。 (有趣的事實,但並不重要。像 PCIe 這樣的串行總線協議不會阻止它成為原子,只要單個消息足夠大。而且由於內存控制器是唯一可以直接與 DRAM 通信的東西,它在內部做什么並不重要,只是它與 CPU 其余部分之間的傳輸大小)。 但無論如何,這是“免費”部分:不需要臨時阻塞其他請求來保持原子傳輸原子性。

x86 保證最多 64 位的對齊加載和存儲是原子的,但不是更廣泛的訪問。 低功耗實現可以自由地將向量加載/存儲分解為 64 位塊,就像從 PIII 到奔騰 M 的 P6 所做的那樣。


原子操作發生在緩存中

請記住,原子只是意味着所有觀察者都將其視為已發生或未發生,而不是部分發生。 沒有要求它實際上立即到達主內存(或者根本不要求,如果很快被覆蓋)。 以原子方式修改或讀取 L1 緩存足以確保任何其他內核或 DMA 訪問將看到對齊的存儲或加載作為單個原子操作發生。 如果這種修改發生在 store 執行后很長時間(例如,由於無序執行而延遲到 store 退休),那也沒關系。

像 Core2 這樣到處都有 128 位路徑的現代 CPU 通常具有原子 SSE 128b 加載/存儲,超出了 x86 ISA 的保證。 但請注意多路 Opteron 上的有趣異常可能是由於超傳輸。 這證明原子地修改 L1 緩存不足以為比最窄數據路徑(在這種情況下不是 L1 緩存和執行單元之間的路徑)更寬的存儲提供原子性。

對齊很重要:跨越緩存線邊界的加載或存儲必須在兩個單獨的訪問中完成。 這使它成為非原子的。

x86 保證最多 8 個字節的緩存訪問是原子的,只要它們不跨越AMD/Intel 上的 8B 邊界 (或者僅適用於 P6 及更高版本的英特爾,請勿跨越緩存線邊界)。 這意味着整個緩存線(現代 CPU 上的 64B)在 Intel 上以原子方式傳輸,即使它比數據路徑(Haswell/Skylake 上的 L2 和 L3 之間的 32B)更寬。 這種原子性在硬件中並非完全“免費”,並且可能需要一些額外的邏輯來防止負載讀取僅部分傳輸的緩存行。 盡管緩存行傳輸僅在舊版本失效后才發生,因此在傳輸發生時內核不應該從舊副本中讀取。 AMD 在實踐中可以在更小的邊界上撕裂,這可能是因為使用了 MESI 的不同擴展,可以在緩存之間傳輸臟數據。

對於更廣泛的操作數,例如將新數據原子地寫入結構的多個條目中,您需要使用所有對其進行訪問的鎖來保護它。 (您可以使用帶有重試循環的 x86 lock cmpxchg16b來執行原子 16b 存儲。請注意,沒有 mutex 就無法模擬它。)


原子讀-修改-寫是它變得更難的地方

相關:我的回答Can num++ be atomic for 'int num'? 更詳細地介紹了這一點。

每個內核都有一個私有的 L1 緩存,它與所有其他內核(使用MOESI協議)一致。 高速緩存行在高速緩存和主存儲器的級別之間以大小從 64 位到 256 位不等的塊傳輸。 (這些傳輸實際上可能在整個緩存行粒度上是原子的?)

要進行原子 RMW,內核可以將 L1 緩存的一行保持在修改狀態,而無需接受對加載和存儲之間受影響的緩存行的任何外部修改,系統的其余部分會將操作視為原子。 (因此它原子的,因為通常的亂序執行規則要求本地線程將自己的代碼視為按程序順序運行。)

它可以通過在原子 RMW 進行中時不處理任何緩存一致性消息來做到這一點(或者一些更復雜的版本,它允許其他操作具有更多的並行性)。

未對齊的lock操作是一個問題:我們需要其他內核才能看到對兩個緩存行的修改作為單個原子操作發生。 可能需要實際存儲到 DRAM,並獲取總線鎖定。 (AMD 的優化手冊說,當緩存鎖不夠用時,他們的 CPU 會發生這種情況。)

LOCK# 信號(cpu 封裝/插座的引腳)用於舊芯片(用於LOCK前綴原子操作),現在有緩存鎖。 對於更復雜的原子操作,如.exchange.fetch_add你將使用LOCK前綴或其他某種原子指令(cmpxchg/8/16?)進行操作。

相同的手冊,系統編程指南部分:

在 Pentium 4、Intel Xeon 和 P6 系列處理器中,鎖定操作是通過緩存鎖定或總線鎖定來處理的。 如果內存訪問是可緩存的並且僅影響單個緩存行,則會調用緩存鎖,並且在操作期間不會鎖定系統總線和系統內存中的實際內存位置

您可以查看 Paul E. McKenney 的論文和書籍:* 現代微處理器中的內存排序,2007 * 內存障礙:軟件黑客的硬件視圖,2010 * perfbook ,“ 並行編程難如果是,你能做些什么嗎?

和 * 英特爾 64 位架構內存訂購白皮書,2007 年。

x86/x86_64 需要內存屏障來防止負載重新排序。 從第一篇論文:

x86 (..AMD64 與 x86 兼容..)由於 x86 CPU 提供“進程排序”,因此所有 CPU 都同意給定 CPU 寫入內存的smp_wmb() ,因此smp_wmb()原語對於 CPU 來說是空操作[7]。 但是,需要使用編譯器指令來防止編譯器執行會導致跨smp_wmb()原語重新排序的優化。

另一方面,x86 CPU 傳統上沒有為負載提供排序保證,因此smp_mb()smp_rmb()原語擴展為lock;addl 此原子指令充當加載和存儲的屏障。

什么讀取內存屏障(來自第二篇論文):

這樣做的結果是,讀取內存屏障命令僅加載到執行它的 CPU 上,因此讀取內存屏障之前的所有加載似乎都在讀取內存屏障之后的任何加載之前完成。

例如,來自《Intel 64 Architecture Memory Ordering White Paper》

Intel 64 內存排序保證對於以下每個內存訪問指令,無論內存類型如何,組成內存操作似乎都作為單個內存訪問執行: ...讀取或寫入雙字(4 字節)的指令,其地址為在 4 字節邊界上對齊。

Intel 64 位內存排序遵循以下原則: 1. 加載不與其他加載重新排序。 ... 5. 在多處理器系統中,內存排序服從因果關系(內存排序尊重傳遞可見性)。 ... Intel 64 內存排序確保按程序順序顯示加載

另外, mfence定義: http : mfence

對在 MFENCE 指令之前發出的所有從內存加載和存儲到內存指令執行序列化操作。 這種序列化操作保證在程序順序中位於 MFENCE 指令之前的每條加載和存儲指令在 MFENCE 指令之后的任何加載或存儲指令之前變得全局可見。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM