[英]why memory_order_relaxed performance is the same as memory_order_seq_cst
[英]Why set the stop flag using `memory_order_seq_cst`, if you check it with `memory_order_relaxed`?
Herb Sutter 在他的“原子<>武器”演講中展示了原子的幾個示例用法,其中之一歸結為以下內容:(視頻鏈接,時間戳)
一個主線程啟動多個工作線程。
工人檢查停止標志:
while (.stop:load(std:.memory_order_relaxed)) { // Do stuff. }
主線程最終確實stop = true;
(注意,使用 order= seq_cst
),然后加入工人。
Sutter 解釋說使用 order= relaxed
檢查標志是可以的,因為誰在乎線程是否以稍大的延遲停止。
但是為什么stop = true;
在主線程中使用seq_cst
? 幻燈片說它是故意不relaxed
的,但沒有解釋原因。
看起來它會起作用,可能會有更大的停止延遲。
這是性能和其他線程看到標志的速度之間的折衷嗎? 即由於主線程只設置了一次標志,我們還不如使用最強的排序,以盡可能快地傳遞消息?
mo_relaxed
適用於stop
標志的加載和存儲更強大的 memory 訂單也沒有有意義的延遲優勢,即使看到對keep_running
或exit_now
標志的更改的延遲很重要。
IDK 為什么 Herb 認為stop.store
不應該放松; 在他的演講中,他的幻燈片有一條評論說// not relaxed
,但在繼續“是否值得”之前,他沒有說任何關於商店方面的事情。
當然,負載在工作循環中運行,但存儲只運行一次,Herb 真的很喜歡建議堅持使用 SC,除非你有性能方面的原因可以真正證明使用其他東西是合理的。 我希望這不是他唯一的原因; 我發現在嘗試了解 memory 訂單實際上是必要的以及為什么需要時,這無濟於事。 但無論如何,我認為要么是他,要么是他的錯誤。
ISO C++ 標准沒有說明商店在多長時間內可見或有什么影響,只有兩個應該建議:第6.9.2.3 節向前進展
18.實現應確保由原子操作或同步操作分配的最后一個值(按修改順序)將在有限的時間段內對所有其他線程可見。
和33.5.4 順序和一致性 [atomics.order]僅涵蓋原子,不包括互斥鎖等:
11.實現應該使原子存儲在合理的時間內對原子負載可見。
另一個線程可以在其負載實際看到此存儲值之前任意循環多次,即使它們都是seq_cst
,假設它們之間沒有任何其他類型的同步。 低線程間延遲是一個性能問題,而不是正確性/正式保證。
而非無限的線程間延遲顯然只是一個“應該”的 QOI(實現質量)問題。 :P 標准中沒有任何內容表明seq_cst
將有助於存儲可見性可能無限期延遲的實現,盡管有人可能會猜測可能是這種情況,例如在具有顯式緩存刷新而不是緩存一致性的假設實現中。 (盡管這樣的實現在 CPU 的性能方面可能實際上不可用,就像我們現在所擁有的那樣;每次釋放和/或獲取操作都必須刷新整個緩存。)
在真實硬件上(使用某種形式的 MESI 緩存一致性),不同的 memory 存儲或加載命令不會使存儲更快實時可見,它們只是控制以后的操作是否可以在等待存儲提交的同時變得全局可見從存儲緩沖區到 L1d 緩存。 (在使該行的任何其他副本無效之后。)
從絕對意義上說,更強的訂單和障礙不會讓事情發生得更快,它們只會延遲其他事情,直到它們被允許相對於商店或負載發生。 (這是所有真實世界的 CPU AFAIK 的情況;無論如何,它們總是試圖讓其他內核盡快看到存儲,因此存儲緩沖區不會填滿,並且
另見(我的類似答案):
第二個問答是關於 x86 ,其中從存儲緩沖區提交到 L1d 緩存是按程序順序進行的。 這限制了緩存未命中存儲執行的距離,以及在存儲之后放置釋放或 seq_cst 柵欄以防止以后的存儲(和加載)可能競爭資源的任何可能的好處。 (x86 微架構將在存儲到達存儲緩沖區的頭部之前執行 RFO(讀取所有權),並且普通加載通常會競爭資源以跟蹤我們正在等待響應的 RFO。)但是這些影響在比如退出另一個線程; 只有非常小規模的重新排序。
因為誰在乎線程是否以稍大的延遲停止。
更像是,誰在乎線程是否通過在加載等待檢查完成后不進行加載/存儲來完成更多工作。 (當然,當我們最終加載true
時,如果它在加載結果的錯誤推測分支的陰影下,這項工作將被丟棄。)在分支錯誤預測之后回滾到一致的 state 的成本或多或少獨立於在錯誤預測的分支之外發生了多少已經執行的工作。 它是一個stop
標志,因此其他 CPU 的緩存/內存帶寬浪費的工作總量非常少。
這種措辭聽起來像是acquire
加載或release
存儲實際上會以絕對實時的速度更快地看到存儲,而不僅僅是相對於該線程中的其他代碼。 (事實並非如此)。
好處是當負載產生false
時,循環迭代中的指令級和內存級並行性更高。 並且簡單地避免在獲取或特別是 SC 加載需要額外指令的 ISA 上運行額外指令,尤其是昂貴的 2 路屏障指令(如 PowerPC isync
/ sync
或特別是 ARMv7 dmb ish
完全屏障,即使是獲取),不像 ARMv8 ldapr
或x86 mov
獲取加載指令。 (神箭)
順便說一句,Herb 是正確的, dirty
標志也可以relaxed
,這只是因為閱讀器和任何可能的作者之間的thread.join
同步。 否則,是的,釋放/獲取。
但在這種情況下, dirty
只需要是atomic<>
,因為可能同時存在的寫入器都存儲相同的值,ISO C++ 仍然認為是 data-race UB。 例如,因為硬件競爭檢測的理論可能性會捕獲沖突的非原子訪問。 (或者像clang -fsanitize=thread
這樣的軟件實現)
有趣的事實:C++20 引入了std::stop_token
用作stop
或keep_running
標志。
首先, stop.store(true, mo_relaxed)
在這種情況下就足夠了。
launch_workers()
stop = true; // not relaxed
join_workers()';
為什么
stop = true;
在主線程中使用 seq_cst?
Herb 沒有提到他使用mo_seq_cst
的原因,但讓我們看看幾種可能性。
基於“ not relaxed
”的評論,他擔心stop.store(true, mo_relaxed)
可以用launch_workers()
或join_workers()
重新排序。
由於launch_workers()
是一個釋放操作,而join_workers()
是一個獲取操作,因此兩者的排序約束不會阻止存儲向任一方向移動。
但是,重要的是要注意,對於這種情況,要stop
的存儲是使用mo_relaxed
還是mo_seq_cst
。 即使使用最強的排序mo_seq_cst
(由於沒有其他 SC 操作不比mo_release
強),排序規則仍然允許使用join_workers()
重新排序。
當然,這種重新訂購不會發生,但我的觀點是,商店中更嚴格的訂購限制不會產生影響。
他可以認為順序一致(SC)存儲是一個優勢,因為執行寬松負載的線程將更快地獲取新值(SC 存儲刷新存儲緩沖區)。
但這似乎無關緊要,因為存儲在創建和加入線程之間,這不是一個緊密的循環,或者正如 Herb 所說:“ ..它是否在代碼的性能關鍵區域中,這種開銷很重要?.. "
他還談到了負載:“ ......你不在乎它什么時候到達...... ”
我們不知道真正的原因,但它可能基於您不使用顯式排序參數(這意味着mo_seq_cst
)的編程約定,除非它有所作為,在這種情況下,正如 Herb 解釋的那樣,只有放松的負載會有所不同。
例如,在弱序 PowerPC 平台上, load(mo_seq_cst)
使用(昂貴的) sync
和(更便宜的) isync
指令, load(mo_acquire)
仍然使用isync
,而load(mo_relaxed)
一個都不使用。 在一個緊密的循環中,這是一個很好的優化。
另外值得一提的是,在主流的X86
平台上, load(mo_seq_cst)
和load(mo_relaxed)
在性能上並沒有真正的區別
就我個人而言,我喜歡這種編程風格,當排序參數無關緊要時省略它們,而在它們產生影響時使用它們。
stop.store(true); // ordering irrelevant, but uses SC
stop.store(true, memory_order_seq_cst); // store requires SC ordering (which is rare)
這只是風格問題。對於兩個商店,編譯器將生成相同的程序集。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.