我現在正試圖解決記憶障礙的問題。我一直在閱讀和觀看有關該主題的視頻,我想確保自己理解正確,并提出一兩個問題。
我從準確理解問題開始。讓我們以以下經典示例作為討論的基礎:假設我們有 2 個執行緒在 2 個不同的內核上運行
這是偽代碼!
我們從這些執行緒開始,int f = 0; int x = 0;然后運行:
# Thread 1
while(f == 0);
print(x)
# Thread 2
x = 42;
f = 1;
當然,這個程式想要的結果是執行緒 1 將列印 42。
注意:我不討論“編譯時重新排序”,我只想關注運行時發生的事情,所以忽略編譯器可能做的各種優化。
好的,據我了解,這里的問題是所謂的“記憶體重新排序”:只要最終結果是程式所期望的,CPU 就可以自由地重新排序記憶體操作。在這種情況下,在執行緒 2 中,f = 1可能會在x = 42. 在這種情況下,執行緒 1 將列印 0,這不是程式員想要的。
此時,維基百科指出了另一種可能發生的情況:
類似地,執行緒#1 的加載操作可能會亂序執行,并且有可能在檢查 f 之前讀取 x
由于我們現在談論的是“亂序執行”——讓我們暫時忽略內核快取。那么讓我們來分析一下這里發生了什么。從執行緒 2 開始 - 編譯后的指令看起來(在偽匯編中)類似于:
1 put 42 into register1
2 write register1 to memory location of x
3 put 1 into register 2
4 write register2 to memory location of f
Ok so I understand that 3-4 may be executed before 1-2. But I don't understand the equivalent in thread 1:
Let's say the instructions of thread 1 will be something like:
1 load f to register1
2 if f is 0 - jump to 1
3 load x to register2
4 print register2
What exactly may be out of order here? 3 can be before 1-2?
Let's go on: Up until now we talked about out-of-order execution, which brings me to my primary confusion:
In this great post the author describes the problem as such: Each core has its own cache, and the core does the memory operations against the cache, not against the main memory. The movement of memory from the core-specific caches to the main memory (or a shared cache) occurs in unpredictable time and order. So in our example - even if thread 2 will execute its instructions in-order - the writing of x=42 will occur before f=1, but that will be only to the cache of core2. The movement of these values to a shared memory may be in the opposite order, and hence the problem.
So I don't understand - when we talk about "memory reordering" - do we talk about Out-of-order execution, or are we talking about the movement of data across caches?
uj5u.com熱心網友回復:
當我們談論“記憶體重新排序”時——我們是在談論亂序執行,還是在談論跨快取的資料移動?
當執行緒以特定順序觀察值的變化時,從程式員的角度來看,無法區分這是否是由于加載的無序執行、存盤緩沖區相對于加載延遲存盤并可能讓它們無序提交(無論執行順序如何),或(假設在沒有一致快取的 CPU 中)快取同步。
或者甚至通過在邏輯核心之間轉發存盤資料而不通過快取,在它提交到快取并變得對所有核心可見之前。 一些 POWER CPU 在現實生活中可以做到這一點,但很少有其他的。
真正的 CPU 具有一致的快取;一旦一個值提交到快取,它對所有內核都是可見的;在其他副本已經失效之前它不會發生,因此這不是讀取“陳舊”資料的機制。現實世界 CPU 上的記憶體重新排序是發生在內核中的事情,連貫快取的讀取和寫入可能以與程式順序不同的順序發生。快取不同步后不會重新同步;它首先保持一致性。
無論機制如何,重要的影響是另一個執行緒觀察您正在讀取/寫入的相同變數,可以看到效果以與匯編程式順序不同的順序發生。
uj5u.com熱心網友回復:
您的兩個郵件問題都有相同的答案(是的!),但原因不同。
首先讓我們看一下這段特殊的偽機器代碼
假設執行緒 1 的指令類似于:
1 load f to register1 2 if f is 0 - jump to 1 3 load x to register2 4 print register2這里到底有什么問題?3可以在1-2之前嗎?
要回答您的問題,這是一個回蕩的“是!”。由于 的內容與 CPUregister1的內容沒有任何關聯,因此register2可以愉快地(并且正確地) preload register2,因此當 1,2 回圈最終中斷時,它可以立即轉到 4。
舉一個實際的例子,register1可能是一個連接到輪詢串行時鐘的 I/O 外設暫存器,而 CPU 只是等待時鐘轉換為低電平,以便它可以將下一個值按位發送到資料輸出線上。這樣做可以節省資料獲取的寶貴時間,更重要的是可以避免外圍資料總線上的爭用。
所以,是的,這種重新排序是非常好的并且是允許的,即使優化關閉并發生在單執行緒、單核 CPU 上。在回圈中斷后,確保register2絕對讀取的唯一方法是插入屏障。
第二個問題是關于快取一致性的。再一次,需要記憶體屏障的答案是“是的!你需要它們”。快取一致性是一個問題,因為現代 CPU 不直接與系統記憶體通信,而是通過它們的快取。只要您只處理單個 CPU 內核和單個快取,一致性就不是問題,因為在同一個內核上運行的所有執行緒都針對同一個快取作業。但是,當您擁有多個具有獨立快取的內核時,它們對系統記憶體內容的各個視圖可能會有所不同,并且需要某種形式的記憶體一致性模型。通過顯式插入記憶體屏障,或在硬體級別上。
uj5u.com熱心網友回復:
在我看來,你錯過了最重要的事情!
由于編譯器沒有看到該更改x也沒有f任何副作用,因此編譯器也可以將所有這些優化掉。并且帶有條件的回圈也f==0將導致“無”,因為編譯器只看到您f=0之前傳播了一個常量,它可以假設這f==0將始終為真并將其優化掉。
對于所有這些,你必須告訴編譯器將會發生一些從給定的代碼流中看不到的事情。這可能類似于呼叫某些信號量/互斥量/...或其他 IPC 功能或使用atomicvars。
如果你編譯你的代碼,我假設你或多或少地得到“沒有”,因為對于兩個代碼部分中的每一個都沒有任何影響,并且編譯器沒有看到變數是從兩個執行緒背景關系中使用的并且優化了所有的東西。
如果我們按照以下示例實作代碼,我們會看到它失敗并0在我的系統上列印。
int main()
{
int f = 0;
int x = 0;
std::thread s( [&f,&x](){ x=42; f=1; } );
while( f==0);
std::cout << x << std::endl;
s.join();
}
如果我們更改int f = 0;為,std::atomic<int> f = 0我們會得到預期的結果。
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/450947.html
標籤:c assembly memory-barriers instructions
上一篇:MongooseError:操作`x.findOne()`緩沖在10000毫秒后超時
下一篇:錯誤的基準,令人費解的組裝
