簡體   English   中英

x86 mfence 和 C++ 內存屏障

[英]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+mfencexchg 。另外我的回答是關於 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.

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