簡體   English   中英

為x86編譯C ++時,StoreStore重新排序發生

[英]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它們的加載順序也是不確定的 我不確定xy的評估之間是否有序列點 如果不是,則與您評估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.

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