[英]Can atomic RMW seq_cst followed and preceded by an atomic load acquire be reordered on x86?
[英]Can non-atomic-load be reordered after atomic-acquire-load?
眾所周知,自 C++11 以來,有 6 個內存順序,在關於std::memory_order_acquire
文檔中:
memory_order_acquire
具有此內存順序的加載操作對受影響的內存位置執行獲取操作:在此加載之前,當前線程中的內存訪問不能重新排序。 這確保了釋放相同原子變量的其他線程中的所有寫入在當前線程中都是可見的。
1、非原子加載可以在atomic-acquire-load之后重新排序:
即它不保證非原子加載不能在獲取原子加載后重新排序。
static std::atomic<int> X;
static int L;
...
void thread_func()
{
int local1 = L; // load(L)-load(X) - can be reordered with X ?
int x_local = X.load(std::memory_order_acquire); // load(X)
int local2 = L; // load(X)-load(L) - can't be reordered with X
}
可以加載int local1 = L;
在X.load(std::memory_order_acquire);
之后重新排序X.load(std::memory_order_acquire);
?
2、我們可以認為,在atomic-acquire-load之后不能重新排序non-atomic-load:
一些文章包含一張圖片,展示了獲取-釋放語義的本質。 這很容易理解,但可能會引起混淆。
例如,我們可能認為std::memory_order_acquire
不能對任何一系列的 Load-Load 操作進行重新排序,甚至在 atomic-acquire-load 之后也不能重新排序非原子加載。
3、非原子加載可以在atomic-acquire-load之后重新排序:
澄清了一件好事:獲取語義可以防止讀取 - 獲取的內存重新排序,以及按照程序順序跟隨它的任何讀取或寫入操作。 http://preshing.com/20120913/acquire-and-release-semantics/
但也知道:在強排序系統( x86 、SPARC TSO、IBM 大型機)上,發布-獲取排序對於大多數操作是自動的。
而 Herb Sutter 在第 34 頁顯示: https ://onedrive.live.com/view.aspx?resid = 4E86B0CF20EF15AD!24884&app=WordPdf&authkey =!AMtj_EflYn2507c
4.即再次,我們可以認為在atomic-acquire-load之后不能重新排序non-atomic-load:
即 x86:
那么在 C++11 中可以在原子獲取加載之后重新排序非原子加載嗎?
我相信這是在 C++ 標准中推理您的示例的正確方法:
X.load(std::memory_order_acquire)
(我們稱之為“操作(A)
”)可能與X
上的某個釋放操作同步(操作(R)
) - 粗略地說,將值分配給X
的操作(A)
正在閱讀。[atomics.order]/2對原子對象
M
執行釋放操作的原子操作A
與對M
執行獲取操作的原子操作B
同步,並從以A
為首的釋放序列中的任何副作用中獲取其值。
這種同步關系可能有助於在L
某些修改和賦值local2 = L
之間建立發生之前的關系。 如果該修改L
之前發生(R)
然后,由於這一事實,即(R)
同步-與(A)
和(A)
進行測序-之前的讀出L
,的該修改L
之前發生這個讀的L
。
但是(A)
對賦值local1 = L
沒有任何影響。 它既不會導致涉及此分配的數據競爭,也不會幫助防止它們。 如果程序是無競爭的,那么它必須采用某種其他機制來確保L
修改與這次讀取同步(如果它不是無競爭的,那么它表現出未定義的行為,標准沒有進一步說明它)。
在 C++ 標准的四個角落里談論“指令重新排序”是沒有意義的。 人們可能會談論由特定編譯器生成的機器指令,或者特定 CPU 執行這些指令的方式。 但從標准的角度來看,這些只是無關的實現細節,只要編譯器和 CPU 產生與標准描述的抽象機器的一種可能執行路徑一致的可觀察行為(假設規則)。
您引用的參考資料非常清楚:您不能在此加載之前移動讀取。 在你的例子中:
static std::atomic<int> X;
static int L;
void thread_func()
{
int local1 = L; // (1)
int x_local = X.load(std::memory_order_acquire); // (2)
int local2 = L; // (3)
}
memory_order_acquire
意味着 (3) 不能在 (2) 之前發生((2) 中的加載順序在 (3) 中的 thr 加載之前)。 它沒有說明(1)和(2)之間的關系。
具有此內存順序的加載操作對受影響的內存位置執行獲取操作:在此加載之前,當前線程中的內存訪問不能重新排序。
這就像編譯器代碼生成的經驗法則。
但這絕對不是 C++ 的公理。
有很多情況,有些可以簡單地檢測到,有些需要更多的工作,其中 V 上的內存 Op 上的操作可以證明可以用 A 上的原子操作 X 重新排序。
最明顯的兩種情況:
(請注意,編譯器的這兩個重新排序對於為 X 指定的任何可能的內存排序都是有效的。)
在任何情況下,轉換都是不可見的,它不會改變有效程序的可能執行。
這些類型的代碼轉換有效的情況不太明顯。 有些是人為的,有些是現實的。
我可以很容易地想出這個人為的例子:
using namespace std;
static atomic<int> A;
int do_acq() {
return A.load(memory_order_acquire);
}
void do_rel() {
A.store(0, memory_order_release);
} // that's all folks for that TU
注意:
使用靜態變量能夠看到對對象的所有操作,對單獨編譯的代碼; 訪問原子同步對象的函數不是靜態的,可以從所有程序中調用。
作為同步原語,A 上的操作建立同步關系:之間存在一個:
do_rel()
線程 Xdo_acq()
線程 Y 有一個明確定義的 A 的修改順序 M 對應於不同線程中對do_rel()
的調用。 每次調用do_acq()
要么:
do_rel()
處調用do_rel()
的結果,並通過在 pX_i 處拉入 X 的歷史記錄與線程 X 同步另一方面,該值始終為 0,因此調用代碼僅從do_acq()
獲取 0,無法從返回值確定發生了什么。 它可以先驗地知道 A 的修改已經發生,但它不能只知道后驗。 先驗知識可以來自另一個同步操作。 先驗知識是線程 Y 歷史的一部分。 無論哪種方式,獲取操作都沒有知識,也不會添加過去的歷史:獲取操作的已知部分是空的,它不能可靠地獲取任何在線程 Y 在 pY_i 的過去。 所以 A 上的獲取是沒有意義的,可以優化掉。
換句話說:當do_acq()
看到 Y 歷史中最近的do_rel()
時,一個對 M 的所有可能值都有效的程序必須是有效的,那個在 A 的所有可以看到的修改之前。 所以 do_rel() 一般不添加任何內容: do_rel()
可以在某些執行中添加一個非冗余的同步,但它添加的最小值 Y 什么都沒有,所以是一個正確的程序,一個沒有競爭條件的程序(表示為:它的行為取決於 M,例如它的正確性是獲取 M 的允許值的某個子集的函數)必須准備好處理從do_rel()
獲取任何do_rel()
; 所以編譯器可以使do_rel()
成為 NOP。
[注意:該論點不能輕易推廣到所有讀取 0 並存儲 0 的 RMW 操作。它可能不適用於 acq-rel RMW。 換句話說,acq+rel RMW 比單獨的加載和存儲更強大,因為它們的“副作用”。]
總結:在那個特定的例子中,不僅內存操作可以相對於原子獲取操作上下移動,原子操作可以完全刪除。
只是為了回答您的標題問題:是的,任何負載(無論是原子負載還是非原子負載)都可以在原子負載之后重新排序。 類似地,任何存儲都可以在原子存儲之前重新排序。
但是,原子存儲不一定允許在原子加載之后重新排序,反之亦然(原子加載在原子存儲之前重新排序)。
44:00 左右見 Herb Sutter 的演講。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.