[英]StoreStore reordering happens when compiling C++ for x86
while(true) {
int x(0), y(0);
std::thread t0([&x, &y]() {
x=1;
y=3;
});
std::thread t1([&x, &y]() {
std::cout << "(" << y << ", " <<x <<")" << std::endl;
});
t0.join();
t1.join();
}
首先,由於數據競爭,我知道它是UB。 但是,我只期望以下輸出:
(3,1), (0,1), (0,0)
我堅信不可能獲得(3,0)
,但是我做到了。 所以我很困惑-畢竟x86不允許StoreStore重新排序 。
所以x = 1
應該在y = 3
之前是全局可見的
我想從理論上講,由於x86內存模型,不可能輸出(3,0)
。 我想它的出現是因為UB。 但我不確定。 請解釋。
除了StoreStore重新排序之外,還有什么可以解釋得到(3,0)
?
您正在用內存不足的C ++語言編寫代碼。 您沒有做任何事情來防止在編譯時重新排序 。
如果查看匯編,您可能會發現存儲的發生順序與源相反,並且/或者負載發生的順序與預期相反。
加載在源中沒有任何順序:即使編譯器是std::atomic
類型,編譯器也可以在y之前先加載x:
t2 <- x(0)
t1 -> x(1)
t1 -> y(3)
t2 <- y(3)
這甚至不是“重新”排序,因為首先沒有定義的順序:
std::cout << "(" << y << ", " <<x <<")" << std::endl;
不一定要在x
之前評估y
。 <<
運算符具有從左到右的關聯性,而運算符重載僅是
op<<( op<<(op<<(y),x), endl); // omitting the string constants.
由於函數參數的求值順序是不確定的 (即使我們正在談論嵌套函數調用 ),因此編譯器可以在求op<<(y)
之前自由求x
值。 在IIRC中,gcc通常只是從右到左求值,如果有必要,會將args推入堆棧的順序匹配。 鏈接問題的答案表明通常是這種情況。 但是,當然,這種行為絕不能得到任何保證。
即使它們是std::atomic
它們的加載順序也是不確定的 。 我不確定x
和y
的評估之間是否有序列點 。 如果不是,則與您評估x+y
:編譯器可以自由地以任何順序評估操作數,因為它們是無序列的。 如果有一個序列點,那么就存在一個順序,但是不確定哪個順序(即,它們是不確定地排序的)。
稍微相關: gcc不會對表達式評估中的非內聯函數調用進行重新排序,以利用C使得評估順序未指定的事實 。 我認為在內聯之后它的優化效果更好,但是在這種情況下,您沒有給出任何理由支持在x
之前加載y
。
關鍵點在於, 編譯器決定重新排序的原因並不重要,只不過允許這樣做 。 如果您沒有強加所有必需的訂購要求,則您的代碼有很多問題,請多加注意。 它是否可以與某些帶有特定周圍代碼的編譯器一起使用並不重要; 那只是意味着這是一個潛在的錯誤。
有關其工作方式/原因的文檔,請參見http://en.cppreference.com/w/cpp/atomic/atomic :
// Safe version, which should compile to the asm you expected.
while(true) {
int x(0); // should be atomic, too, because it can be read+written at the same time. You can use memory_order_relaxed, though.
std::atomic<int> y(0);
std::thread t0([&x, &y]() {
x=1;
// std::atomic_thread_fence(std::memory_order_release); // A StoreStore fence is an alternative to using a release-store
y.store(3, std::memory_order_release);
});
std::thread t1([&x, &y]() {
int tx, ty;
ty = y.load(std::memory_order_acquire);
// std::atomic_thread_fence(std::memory_order_acquire); // A LoadLoad fence is an alternative to using an acquire-load
tx = x;
std::cout << ty + tx << "\n"; // Don't use endl, we don't need to force a buffer flush here.
});
t0.join();
t1.join();
}
為了使“ 獲取/發布”語義能夠為您提供所需的訂購,最后一個存儲必須是發布存儲,而“獲取負載”必須是第一個負載。 這就是為什么我將y
設為std :: atomic的原因,即使您將x設置為0或1更像是一個標志。
如果您不想使用發布/獲取,則可以在商店之間放置StoreStore圍欄,並在負載之間放置LoadLoad圍欄。 在x86上,這只會阻止編譯時重新排序,但是在ARM上,您會收到一條內存屏障指令。 (請注意,從技術上講, y
仍然需要原子性來遵守C的數據爭用規則,但是您可以在其上使用std::memory_order_relaxed
。)
實際上,即使使用y
發布/獲取順序, x
應該是原子的 。 即使我們看到y==0
,x的負載仍然會發生。 因此,在線程2中讀取x與在線程1中寫入y不同步,因此它是UB。 實際上, 在x86(和大多數其他體系結構)上的int
加載/存儲是atomic 。 但是請記住std::atomic
包含其他語義,例如可以通過其他線程異步更改值的事實。
如果您在存儲i
和-i
或其他內容的一個線程內循環,而在另一線程內循環檢查abs(y)始終> = abs(x),則硬件重新排序測試的運行速度可能會更快。 每個測試創建和銷毀兩個線程的開銷很大。
當然,要正確處理此問題,您必須知道如何使用C生成所需的asm(或直接在asm中編寫)。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.