簡體   English   中英

使用 memory_order_relaxed 如何在典型架構上保證原子變量的修改總順序?

[英]With memory_order_relaxed how is total order of modification of an atomic variable assured on typical architectures?

據我了解, memory_order_relaxed是為了避免昂貴的 memory 柵欄,這可能需要在特定架構上進行更嚴格的排序。 在那種情況下,如何在流行的處理器上實現原子變量的總修改順序?

編輯:

atomic<int> a;

void thread_proc()
{
    int b = a.load(memory_order_relaxed);
    int c = a.load(memory_order_relaxed);
    printf(“first value %d, second value %d\n, b, c);
}

int main()
{

    thread t1(thread_proc);
    thread t2(thread_proc);
    a.store(1, memory_order_relaxed);
    a.store(2, memory_order_relaxed);
    t1.join();
    t2.join();
}

什么可以保證 output 不會:

first value 1, second value 2
first value 2, second value 1

?

多處理器通常使用MESI 協議來確保一個位置的總存儲順序。 信息以高速緩存行粒度傳輸。 該協議確保在處理器修改高速緩存行的內容之前,所有其他處理器放棄它們的行副本,並且必須重新加載修改后的行的副本。 因此,在處理器將 x 和 y 寫入同一位置的示例中,如果任何處理器看到 x 的寫入,則它必須已從修改的行重新加載,並且必須在寫入器寫入 y 之前再次放棄該行。

通常有一組特定的匯編指令對應於 std::atomics 上的操作,例如 x86 上的原子加法是lock xadd

通過指定memory order relaxed ,您可以在概念上將其視為告訴編譯器“您必須使用此技術來增加值,但除此之外,我沒有在標准優化規則之外施加其他限制”。 因此,在寬松的排序約束下,從字面上看,只需將add替換為lock xadd就足夠了。

另請記住,“memory_order_relaxed”指定了編譯器必須遵守的最低標准。 某些平台上的一些內在函數將具有隱含的硬件障礙,這不會因為過於受約束而違反約束。

所有原子操作都符合[intro.races]/14

如果修改原子 object M 的操作 A 發生在修改 M 的操作 B 之前,則在 M 的修改順序中 A 應早於 B。

主線程中的兩個存儲必須按該順序發生,因為這兩個操作是在同一個線程中排序的 因此,它們不能在該順序之外發生。 如果有人在原子中看到值 2,那么第一個線程必須執行超過值設置為 1 的點,根據[intro.races]/4

對特定原子 object M 的所有修改都以某個特定的總順序發生,稱為 M 的修改順序。

這當然只適用於特定原子 object 上的原子操作; 使用relaxed排序時,不存在相對於其他事物的排序(這就是重點)。

這是如何在真機上實現的? 以編譯器認為合適的任何方式這樣做。 編譯器可以決定,因為您正在覆蓋剛剛設置的變量的值,所以它可以根據 as-if 規則刪除第一個存儲。 根據 C++ memory model,沒有人看到值 1 是完全合法的實現。

但除此之外,編譯器需要發出使其工作所需的任何內容。 請注意,通常不允許亂序處理器亂序完成相關操作,因此這通常不是問題。

線程間通信有兩個部分:

  • 可以進行加載和存儲的核心
  • memory 系統由一致的緩存組成

問題在於 CPU 內核中的推測執行。

處理器加載和存儲單元總是需要比較地址以避免將兩次寫入重新排序到同一位置(如果它重新排序寫入)或預取剛剛寫入的陳舊值(當讀取提前完成時) ,在之前的寫入之前)。

如果沒有該功能,任何可執行代碼序列都將面臨其 memory 訪問完全隨機化的風險,看到由以下指令寫入的值等。所有 memory 位置都將以瘋狂的方式“重命名”,程序無法引用連續兩次到同一個(最初命名的)位置

所有程序都會中斷。

另一方面,潛在運行代碼中的 memory 位置可以有兩個“名稱”:

  • 可以保存可修改值的位置,在 L1d
  • 可以解碼為可執行代碼的位置,在 L1i 中

在執行特殊的重新加載代碼指令之前,這些都不會以任何方式連接,不僅 L1i,而且指令解碼器都可以在緩存位置中具有其他可修改的位置。

[另一個復雜情況是當兩個虛擬地址(由推測加載或存儲使用)引用相同的物理地址(別名)時:這是另一個需要處理的沖突。]

總結:在大多數情況下,一個CPU自然會提供一個命令來訪問每個數據memory位置。

編輯:

雖然核心需要跟蹤使推測執行無效的操作,主要是對稍后由推測指令讀取的位置的寫入。 讀取不會相互沖突,CPU 內核可能希望在推測性讀取(使讀取提前明顯發生)之后跟蹤緩存 memory 的修改,並且如果讀取可以無序執行,則可以想象以后的讀取可能在較早閱讀之前完成 關於為什么系統會先開始稍后讀取,一個可能的原因是地址計算是否更容易且首先完成。

因此,一個可以開始無序讀取的系統,一旦緩存提供了一個值,就會認為它們已完成,並且只要同一個核心沒有寫入最終與任一讀取發生沖突,並且不監控就有效另一個 CPU 想要修改關閉的 memory 位置(可能是一個位置)導致 L1i 緩存失效,這樣的順序是可能的:

  • 將即將執行的指令分解為序列 A,它是一個很長的序列操作列表,以 r1 中的結果結束,B 是一個較短的序列,以 r2 中的結果結束
  • 兩者並行運行,B 較早產生結果
  • 推測性地嘗試load (r2) ,注意寫入該地址可能會使推測無效(假設該位置在 L1i 中可用)
  • 然后另一個 CPU 讓我們惱火,竊取了 (r2) 的緩存行保存位置
  • A 完成使 r1 值可用,我們可以推測性地load (r1) (恰好與 (r2) 的地址相同); 直到我們的緩存取回它的緩存行,它才會停止
  • 最后完成的負載的值可以與第一個不同

A 和 B 的推測都沒有使任何 memory 位置無效,因為系統不認為緩存行的丟失或最后一次加載時返回不同的值是推測的無效(這很容易實現為我們在本地擁有所有信息)。

在這里,系統將任何讀取視為與不是本地寫入的任何本地操作不沖突,並且加載按順序完成,具體取決於 A 和 B 的復雜性,而不是程序順序中的先到者(上面的描述沒有甚至沒有說程序順序發生了變化,只是被猜測忽略了:我從未描述過哪些負載是程序中的第一個)。

因此,對於輕松的原子負載,此類系統需要特殊指令

緩存系統

當然,緩存系統不會改變請求的順序,因為它就像一個全局隨機訪問系統,由內核臨時擁有。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

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