[C++] C ++ 11引入了標準化的內存模型。 這是什麼意思? 它將如何影響C ++編程?


Answers

我將給出我理解內存一致性模型(或內存模型,簡稱)的類比。 它受Leslie Lamport的開創性論文“時間,時鐘和分佈式系統中的事件排序”的啟發。 類比是恰當的,具有根本性的意義,但對許多人來說可能是矯枉過正的。 但是,我希望它提供了一個能夠促進關於內存一致性模型推理的心理圖像(圖形表示)。

我們來看一個空間 - 時間圖中所有存儲位置的歷史,其中水平軸表示地址空間(即每個存儲位置由該軸上的一個點表示),而垂直軸表示時間(我們將看到,總的來說,沒有普遍的時間觀念)。 因此,每個存儲器位置所保存的值的歷史記錄由該存儲器地址處的垂直列表示。 每個值的變化都是由於其中一個線程向該位置寫入新值。 通過內存映像 ,我們將指特定線程 在特定時間可觀察到的所有內存位置值的集合/組合。

“內存一致性和緩存一致性入門”引用

直觀的(也是最具限制性的)內存模型是順序一致性(SC),其中多線程執行看起來像是每個組成線程的順序執行的交錯,就好像線程在單核處理器上時間復用一樣。

全局記憶順序可以從程序的一次運行到另一次不同,並且可能不會事先知道。 SC的特徵是地址空間 - 時間圖中表示同時性平面 (即存儲器圖像)的一組水平切片。 在給定的平面上,其所有事件(或存儲器值)都是同時發生的。 有絕對時間的概念,其中所有線程都同意哪些存儲器值是同時存在的。 在SC中,在每個時刻,所有線程隻共享一個內存映像。 也就是說,在任何時刻,所有處理器都會同意內存映像(即內存的聚合內容)。 這不僅意味著所有線程都查看所有內存位置的相同值序列,而且所有處理器都觀察到所有變量的相同組合 。 這與所有線程的所有內存操作(在所有內存位置上)都以相同的順序進行觀察是一樣的。

在寬鬆的內存模型中,每個線程都會以自己的方式切分地址空間時間,唯一的限制是每個線程的切片不能相互交叉,因為所有線程必須同意每個單獨內存位置的歷史(當然,不同線程的切片可以並且將會彼此交叉)。 沒有通用的方法來分割它(沒有地址空間時間的特權化)。 切片不必是平面的(或線性的)。 它們可以是彎曲的,這可以使得線程讀取由另一個線程寫入的數據,而不用寫入它們的順序。不同存儲器位置的歷史可以在由任何特定線程查看時相對於彼此任意地滑動(或拉伸) 。 每個線程對於哪些事件(或等價的存儲器值)是同時存在不同的意義。 與一個線程同時發生的一組事件(或內存值)不同時發生。 因此,在寬鬆的內存模型中,所有線程仍然觀察每個內存位置的相同歷史(即值序列)。 但他們可能觀察到不同的記憶圖像(即,所有記憶位置的值的組合)。 即使兩個不同的存儲位置是由同一個線程按順序寫入的,這兩個新寫入的值可能會被其他線程以不同的順序觀察到。

[來自維基百科的圖片]

熟悉愛因斯坦的相對論的讀者會注意到我所指的是什麼。 將Minkowski的話翻譯成內存模型領域:地址空間和時間是地址空間時間的陰影。 在這種情況下,每個觀察者(即線程)都會將事件(即內存存儲/加載)的陰影投影到他自己的世界線(即他的時間軸)和他自己的同時性平面(他的地址空間軸) 。 C ++ 11內存模型中的線程對應於在狹義相對論中彼此相對移動的觀察者 。 序貫一致性對應於伽利略時空 (即所有觀察者都同意事件的一個絕對秩序和全局同時性感)。

記憶模型和狹義相對論之間的相似之處源於這兩個事實,它們都定義了一組部分有序的事件,通常稱為因果集。 某些事件(即記憶存儲)可能會影響(但不會受其他事件影響)。 一個C ++ 11線程(或物理觀察者)不過是一個鏈(即一個完全有序的集)的事件(例如,內存加載和存儲到可能不同的地址)。

在相對論中,一些秩序恢復到部分有序事件的看似混沌的圖景,因為所有觀察者都同意的唯一時間順序是“時間性”事件之間的排序(即那些原則上可以被任何粒子變得更慢的事件所連接的事件比光在真空中的速度)。 只有時間般的相關事件是不變的命令。 物理時間,克雷格卡倫德

在C ++ 11內存模型中,類似的機制(獲取 - 釋放一致性模型)用於建立這些本地因果關係

為了提供內存一致性的定義和放棄SC的動機,我將引用“內存一致性和高速緩存一致性入門”

對於共享內存機器,內存一致性模型定義其內存系統的架構可見行為。 單個處理器核心的正確性標准在“ 一個正確的結果 ”和“ 許多不正確的替代方案 ”之間劃分行為。 這是因為處理器的體系結構要求線程的執行將給定的輸入狀態轉換為單一明確定義的輸出狀態,即使是在無序的內核上。 然而,共享內存一致性模型涉及多線程的加載和存儲,並且通常允許許多正確的執行,而不允許許多(更多)不正確的執行。 多次正確執行的可能性是由於ISA允許多個線程同時執行,通常來自不同線程的許多可能的合法交錯指令。

寬鬆弱的內存一致性模型是由強模型中的大多數內存排序是不必要的。 如果一個線程更新十個數據項然後一個同步標誌,程序員通常不關心數據項是否按照彼此的順序更新,而只是在更新標誌之前更新所有數據項(通常使用FENCE指令實現)。 寬鬆的模型試圖捕獲這種增加的訂購靈活性,並且只保留程序員“ 要求 ”的訂單,以獲得SC的更高性能和正確性。 例如,在某些體系結構中,每個內核使用FIFO寫緩衝區來保存已提交(退役)存儲的結果,然後將結果寫入緩存。 這種優化增強了性能,但違反了SC。 寫入緩衝區隱藏了服務商店未命中的延遲。 因為商店很常見,所以能夠避免大部分停滯是一個重要的好處。 對於單核處理器,通過確保地址A的負載將最新存儲的值返回給A,即使一個或多個存儲器位於寫入緩衝區中,也可以使寫緩衝區在架構上不可見。 這通常是通過將最近存儲到A的值繞過來自A的加載來完成的,其中“最近的”由程序順序確定,或者如果存儲到A的存儲在寫入緩衝器中則阻止A的加載。 當使用多個內核時,每個內核都有自己的旁路寫入緩衝區。 如果沒有寫入緩衝區,硬件是SC,但是使用寫入緩衝區並非如此,因此在多核處理器中可以在架構上顯示寫入緩衝區。

如果一個內核有一個非FIFO寫入緩衝區,允許商店以不同於它們輸入順序的順序離開,那麼可能會發生存儲 - 存儲重新排序。 如果第一個商店在高速緩存中未命中,而第二個命中或第二個商店可能與先前的商店合併(即在第一個商店之前),則可能會發生這種情況。 負載重新排序也可能發生在動態調度的內核上,這些內核執行程序順序之外的指令。 這可以像對另一個核心上的商店重新排序一樣行事(你能想出兩個線程之間的示例交錯?)。 使用較晚的存儲(加載存儲重新排序)重新排序較早的加載可能會導致許多不正確的行為,例如在釋放鎖定後加載一個值(如果存儲為解鎖操作)。 請注意,即使內核按程序順序執行所有指令,也可能由於在通常實現的FIFO寫緩衝區中進行本地旁路而引起存儲器加載重新排序。

由於緩存一致性和內存一致性有時會混淆,因此也有這樣的引用是有益的:

與一致性不同, 緩存一致性對於軟件來說既不可見也不需要。 Coherence試圖使共享內存系統的緩存在功能上不可見,就像單核系統中的緩存一樣。 通過分析加載和存儲的結果,正確的一致性可確保程序員無法確定係統是否以及在何處具有高速緩存。 這是因為正確的一致性確保緩存永遠不會啟用新的或不同的功能行為(程序員仍然可以使用時間信息推斷可能的緩存結構)。 高速緩存一致性協議的主要目的是維護每個內存位置的單寫多讀器(SWMR)不變量。 一致性和一致性之間的一個重要區別是,在每個內存位置基礎上指定了一致性,而針對所有內存位置指定了一致性。

繼續我們的心理圖像,SWMR不變量對應於物理要求,即最多只有一個粒子位於任何一個位置,但可以有任意位置的無限數量的觀察者。

Question

C ++ 11引入了標準化的內存模型,但究竟是什麼意思? 它將如何影響C ++編程?

這篇文章 (由Gavin Clarke引用Herb Sutter )說,

內存模型意味著C ++代碼現在擁有一個標準化的庫來調用,無論編譯器是誰製造的,以及它運行在哪個平台上。 有一種標準的方法來控制不同線程與處理器內存的對話。

Sutter說:“當你談論的是跨標準的不同內核分割代碼時,我們正在談論內存模型。我們將會優化它,而不會破壞人們在代碼中所做的下列假設。

好吧,我可以記住在線提供的這個和類似的段落(因為我從出生開始就有自己的記憶模型:P),甚至可以回答其他人提出的問題,但說實話,我並不完全理解這一點。

所以,我基本上想知道的是,C ++程序員甚至在之前就開發了多線程應用程序,因此,如果它是POSIX線程,Windows線程或C ++ 11線程,它又有什麼關係? 有什麼好處? 我想了解低級細節。

我也感覺到C ++ 11內存模型與C ++ 11多線程支持有某種關係,因為我經常將這兩者結合在一起。 如果是這樣,究竟如何? 他們為什麼要相關?

由於我不知道多線程的內部工作原理,以及一般的內存模型意味著什麼,請幫助我理解這些概念。 :-)




這意味著標準現在定義了多線程,並且它定義了在多線程環境中發生的事情。 當然,人們使用不同的實現,但這就像問我們為什麼應該有一個std::string當我們都可以使用一個home-rolled string類時。

當您談論POSIX線程或Windows線程時,實際上您正在討論x86線程時,這有點虛幻,因為它是一個可同時運行的硬件功能。 無論您使用的是x86還是ARM,還是MIPS ,或其他任何您可以想到的,C ++ 0x內存模型都可以保證。




如果您使用互斥鎖來保護您的所有數據,那麼您確實不需要擔心。 互斥體一直提供足夠的訂購和可視性保證。

現在,如果您使用原子或無鎖算法,則需要考慮內存模型。 內存模型準確地描述了原子提供排序和可視性保證的時間,並為手工編碼保證提供了便攜式圍欄。

以前,原子將使用編譯器內在函數或一些更高級別的庫來完成。 使用特定於CPU的指令(內存屏障)可以完成圍欄。