[英]x86 mfence and C++ memory barrier
我正在檢查編譯器如何為 x86_64 上的多核內存屏障發出指令。 下面的代碼是我正在使用gcc_x86_64_8.3
測試的gcc_x86_64_8.3
。
std::atomic<bool> flag {false};
int any_value {0};
void set()
{
any_value = 10;
flag.store(true, std::memory_order_release);
}
void get()
{
while (!flag.load(std::memory_order_acquire));
assert(any_value == 10);
}
int main()
{
std::thread a {set};
get();
a.join();
}
當我使用std::memory_order_seq_cst
,我可以看到MFENCE
指令與任何優化MFENCE
-O1, -O2, -O3
。 該指令確保刷新存儲緩沖區,從而更新 L1D 緩存中的數據(並使用 MESI 協議確保其他線程可以看到效果)。
但是,當我使用沒有優化的std::memory_order_release/acquire
,也使用了MFENCE
指令,但是使用-O1, -O2, -O3
優化省略了該指令,並且沒有看到其他刷新緩沖區的指令。
在不使用MFENCE
的情況下,如何確保將存儲緩沖區數據提交到高速緩存以確保內存順序語義?
下面是帶有-O3
的 get/set 函數的匯編代碼,就像我們在 Godbolt 編譯器資源管理器中得到的一樣:
set():
mov DWORD PTR any_value[rip], 10
mov BYTE PTR flag[rip], 1
ret
.LC0:
.string "/tmp/compiler-explorer-compiler119218-62-hw8j86.n2ft/example.cpp"
.LC1:
.string "any_value == 10"
get():
.L8:
movzx eax, BYTE PTR flag[rip]
test al, al
je .L8
cmp DWORD PTR any_value[rip], 10
jne .L15
ret
.L15:
push rax
mov ecx, OFFSET FLAT:get()::__PRETTY_FUNCTION__
mov edx, 17
mov esi, OFFSET FLAT:.LC0
mov edi, OFFSET FLAT:.LC1
call __assert_fail
x86 內存排序模型為所有存儲指令1提供了 #StoreStore 和 #LoadStore 屏障,這正是發布語義所需要的。 處理器也會盡快提交一條存儲指令; 當存儲指令退出時,存儲成為存儲緩沖區中最舊的,內核具有處於可寫一致性狀態的目標緩存線,並且緩存端口可用於執行存儲操作2 。 所以不需要MFENCE
指令。 該標志將盡快對另一個線程可見,當它出現時, any_value
保證為 10。
另一方面,順序一致性也需要#StoreLoad 和#LoadLoad 屏障。 MFENCE
需要提供3 個障礙,因此它用於所有優化級別。
相關: 英特爾硬件上的存儲緩沖區大小? 究竟什么是存儲緩沖區? .
腳注:
(1) 有一些例外情況不適用於這里。 特別是,非臨時存儲和存儲到不可緩存的寫入組合內存類型僅提供 #LoadStore 屏障。 無論如何,這些障礙是為在 Intel 和 AMD 處理器上存儲回寫內存類型提供的。
(2) 這與在某些條件下全局可見的寫組合存儲形成對比。 請參閱英特爾手冊第 3 卷的第 11.3.1 節。
(3) 見彼得回答下的討論。
x86 的 TSO 內存模型是順序一致性 + 存儲緩沖區,因此只有 seq-cst 存儲需要任何特殊圍欄。 (在存儲之后停止直到存儲緩沖區耗盡,在以后的加載之前,我們需要恢復順序一致性)。 較弱的 acq/rel 模型與存儲緩沖區引起的 StoreLoad 重新排序兼容。
(請參閱評論中的討論:“允許 StoreLoad 重新排序”是否對 x86 允許的內容進行了准確而充分的描述。內核始終按程序順序查看自己的存儲,因為加載會監聽存儲緩沖區,因此您可以說存儲轉發也重新排序最近存儲的數據負載。除非你不能總是: 全局不可見加載指令)
(和BTW,編譯器比GCC使用其他xchg
做SEQ-CST店。這實際上是更有效的當前的CPU。GCC的mov
+ mfence
可能已經過去便宜,但目前通常更糟,即使你不關心舊值。請參閱為什么具有順序一致性的 std::atomic 存儲使用 XCHG?以比較 GCC 的mov+mfence
與xchg
。另外我的回答是關於 x86上哪個是更好的寫屏障:lock+addl 或xchgl? )
有趣的事實:您可以通過屏蔽 seq-cst加載而不是存儲來實現順序一致性。 但是對於大多數用例來說,廉價負載比廉價商店更有價值,所以每個人都使用 ABI,因為商店的所有障礙都在那里。
有關 C++11 原子操作如何映射到 x86、PowerPC、ARMv7、ARMv8 和 Itanium 的 asm 指令序列的詳細信息,請參見https://www.cl.cam.ac.uk/~pes20/cpp/cpp0xmappings.html 。 另外什么時候需要 x86 LFENCE、SFENCE 和 MFENCE 指令?
當我在沒有優化的情況下使用 std::memory_order_release/acquire 時,也會使用 MFENCE 指令
那是因為flag.store(true, std::memory_order_release);
不內聯,因為您禁用了優化。 這包括內聯非常簡單的成員函數,如atomic::store(T, std::memory_order = std::memory_order_seq_cst)
當__atomic_store_n()
GCC 內置的排序參數是一個運行時變量(在atomic::store()
頭實現中)時, GCC 會保守地播放它並將其提升為 seq_cst。
gcc 對mfence
進行分支實際上可能是值得的,因為它太貴了,但這不是我們得到的。 (但是對於具有運行時變量順序參數的函數,這會產生更大的代碼大小,並且代碼路徑可能不會很熱。因此分支可能只是 libatomic 實現中的一個好主意,或者在極少數情況下使用配置文件引導優化函數足夠大,不能內聯,但采用可變順序。)
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.