10多年前的程式設計師對處理器亂序執行和記憶體屏障應該是很熟悉的,但隨著計算機技術突飛猛進的發展,我們離底層原理越來越遠,這並不是一件壞事,但在有些情況下了解一些底層原理有助於我們更好的工作,比如現代高階語言多提供了多執行緒併發技術,如果不深入下來,那麼有些由多執行緒造成問題就很難排查和理解.
今天準備來聊聊亂序執行技術和記憶體屏障.為了能讓大多數人理解,這裡省略了很多不影響理解的旁枝末節,但由於我個人水平有限,如果不妥之處,希望各位指正.
按順執行技術
在開始說亂序執行之前,得先把按序執行說一遍.在早期處理器中,處理器執行指令的順序就是按照我們編寫彙編**的順序執行的,換句話說此時處理器指令執行順序和我們**順序一致,我們稱之為按序執行(in order execution).我們以燒水泡茶為例來說明按序執行的過程(熟悉的同學會想起華羅庚的統籌學):
洗水壺燒開水
洗茶壺洗茶杯
拿茶葉泡茶
我們假設每一步代表一條指令的執行,此時從指令1到指令6執行的過程就是我們所說的按序執行.整個過程可以表示為:
按序執行對於早期處理器而言是一種行之有效的方案,但隨著對時間的要求,我們希望上述過程能夠在最短的時間內執行完成,這就促使人們迫切希望找到一種優化指令執行過程的方案.考慮上述執行過程,我們發現洗茶壺這步完全沒有必要等待燒開水完成,也就是說洗茶壺和洗水杯完全可以和燒開水同時進行,這麼一來,優化過的流程如圖:
這種通過改變原有執行順序而減少時間的執行過程我們被稱之為亂序執行,也稱為重排.到現在為止,我們已經弄明白了什麼是按序執行,什麼是亂序.那接下來就看看處理器中的亂序執行技術.
亂序執行技術
處理器亂序執行
隨著處理器流水線技術和多核技術的發展,目前的高階處理器通過提高內部邏輯元件的利用率來提高執行速度,通常會採用亂序執行技術.這裡的亂序和上面談到燒水煮茶的道理是一樣的.
先來看一張處理器的簡要結構圖:
處理器從l1 cache中取出一批指令,分析找出那些不存在相互依賴的指令,同時將其發射到多個邏輯單元執行,比如現在有以下幾條指令:
ldr r1, [r0];
add r2, r1, r1;
add r4,r3,r3;12
3通過分析發現第二條指令和第一條指令存在依賴關係,但是和第3條指令無關,那麼處理器就可能將其傳送到兩個邏輯單元去執行,因此上述的指令執行流程可能如下:
可以說亂序執行技術是處理器為提高運算速度而做出違背**原有順序的優化.在單核時代,處理器保證做出的優化不會導致執行結果遠離預期目標,但在多核環境下卻並非如此.
首先多核時代,同時會有多個核執行指令,每個核的指令都可能被亂序;另外,處理器還引入了l1,l2等快取機制,每個核都有自己的快取,這就導致邏輯次序上後寫入記憶體的資料未必真的最後寫入.最終帶來了這麼乙個問題:如果我們不做任何防護措施,處理器最終得出的結果和我們邏輯得出的結果大不相同.比如我們在乙個核上執行資料的寫入操作,並在最後寫乙個標記用來表示之前的資料已經準備好,然後從另乙個核上通過判斷這個標誌來判定所需要的資料已經就緒,這種做法存在風險:標記位先被寫入,但是之前的資料操作卻並未完成(可能是未計算完成,也可能是資料沒有從處理器快取重新整理到主存當中),最終導致另乙個核中使用了錯誤的資料.
編譯器指令重排
除了上述由處理器和快取引起的亂序之外,現代編譯器同樣提供了亂序優化.之所以出現編譯器亂序優化其根本原因在於處理器每次只能分析一小塊指令,但編譯器卻能在很大範圍內進行**分析,從而做出更優的策略,充分利用處理器的亂序執行功能.
亂序的分類
現在來總結下所有可能發生亂序執行的情況:
現代處理器採用指令並行技術,在不存在資料依賴性的前提下,處理器可以改變語句對應的機器指令的執行順序來提高處理器執行速度
現代處理器採用內部快取技術,導致資料的變化不能及時反映在主存所帶來的亂序.
現代編譯器為優化而重新安排語句的執行順序
小結儘管我們看到亂序執行初始目的是為了提高效率,但是它看來其好像在這多核時代不盡人意,其中的某些」自作聰明」的優化導致多執行緒程式產生各種各樣的意外.因此有必要存在一種機制來消除亂序執行帶來的壞影響,也就是說應該允許程式設計師顯式的告訴處理器對某些地方禁止亂序執行.這種機制就是所謂記憶體屏障.不同架構的處理器在其指令集中提供了不同的指令來發起記憶體屏障,對應在程式語言當中就是提供特殊的關鍵字來呼叫處理器相關的指令.
記憶體屏障
處理器亂序規則
上面我們說了處理器會發生指令重排,現在來簡單的看看常見處理器允許的重排規則,換言之就是處理器可以對那些指令進行順序調整:
處理器 load-load load-store store-store store-load 資料依賴
x86 n n n y n
powerpc y y y y n
ia64 y y y y n
**中的y表示前後兩個操作允許重排,n則表示不允許重排.與這些規則對應是的禁止重排的記憶體屏障.
注意:處理器和編譯都會遵循資料依賴性,不會改變存在資料依賴關係的兩個操作的順序.所謂的資料依賴性就是如果兩個操作訪問同乙個變數,且這兩個操作中有乙個是寫操作,那麼久可以稱這兩個操作存在資料依賴性.舉個簡單例子:
a=100;//write
b=a;//read
或者a=100;//write
a=2000;//write
或者a=b;//read
b=12;//write12
3456
78910
以上所示的,兩個操作之間不能發生重排,這是處理器和編譯所必須遵循的.當然這裡指的是發生在單個處理器或單個執行緒中.
記憶體屏障的分類
在開始看一下**之前,務必確保自己了解store和load指令的含義.簡單來說,store就是將處理器快取中的資料重新整理到記憶體中,而load則是從記憶體拷貝資料到快取當中.
屏障型別 指令示例 說明
loadload barriers load1;loadload;load2 該屏障確保load1資料的裝載先於load2及其後所有裝載指令的的操作
storestore barriers store1;storestore;store2 該屏障確保store1立刻重新整理資料到記憶體(使其對其他處理器可見)的操作先於store2及其後所有儲存指令的操作
loadstore barriers load1;loadstore;store2 確保load1的資料裝載先於store2及其後所有的儲存指令重新整理資料到記憶體的操作
storeload barriers store1;storeload;load1 該屏障確保store1立刻重新整理資料到記憶體的操作先於load2及其後所有裝載裝載指令的操作.它會使該屏障之前的所有記憶體訪問指令(儲存指令和訪問指令)完成之後,才執行該屏障之後的記憶體訪問指令
storeload barriers同時具備其他三個屏障的效果,因此也稱之為全能屏障,是目前大多數處理器所支援的,但是相對其他屏障,該屏障的開銷相對昂貴.在x86架構的處理器的指令集中,lock指令可以觸發storeload barriers.
現在我們綜合重排規則和記憶體屏障型別來說明一下.比如x86架構的處理器中允許處理器對store-load操作進行重排,與之對應有storeload barriers禁止其重排.
as-if-serial語義
無論是處理器還是編譯器,不管怎麼重排都要保證(單執行緒)程式的執行結果不能被改變,這就是as-if-serial語義.比如燒水煮茶的最終結果永遠是煮茶,而不能變成燒水.為了遵循這種語義,處理器和編譯器不能對存在資料依賴性的操作進行重排,因為這種重排會改變操作結果,比如對:
a=100;//write
b=a;//read12
重排為:
b=a;
a=100;12
此時b的值就是不正確的.如果不存在操作之間不存在資料依賴,那麼這些操作就可能被處理器或編譯器進行重排,比如:
a=10;
b=200;
result=a*b;12
3它們之間的依賴關係如圖:
由於a=10和b=200之間不存在依賴關係,因此編譯器或處理可以這兩兩個操作進行重排,因此最終執行順序可能有以下兩種情況:
但無論哪種執行順序,最終的結果都是對的.
正是因為as-if-serial的存在,我們在編寫單執行緒程式時會覺得好像它就是按**的順序執行的,這讓我們可以不必關心重排的影響.換句話說,如果你從來沒有編寫多執行緒程式的需求,那就不需要關注今天我所說的一切.
CPU快取和記憶體屏障
cpu效能優化的手段 快取 為了提供程式執行的效能,現代cpu在很多方面對程式進行了優化。例如cpu快取記憶體。盡可能避免處理器訪問主記憶體的時間開銷,處理器大多數會利用快取以提高效能。cpu快取分為3級快取,l1,l2,l3,l1的訪問速度最快,然後遞減。如果機器是多核,則每個cpu對應相對的l1...
CPU快取和記憶體屏障
為了提高程式的執行效能,現代cpu在很多方面對程式進行了優化 例如 cpu快取記憶體,盡可能的避免處理器訪問主記憶體的時間開銷,處理器大多會利用快取以提高效能 l1 cache 一級快取 是cpu第一層快取記憶體,分為資料快取和指令快取,一般伺服器cpu的l1快取的容量通常在32 4096kb l2...
CPU快取和記憶體屏障
cpu效能優化手段 執行時指令重排 為了提高程式的執行效能,現代cpu在很多方面對程式進行了優化 例如 cpu快取記憶體,盡可能的避免處理器訪問主記憶體的時間開銷,處理器大多會利用快取以提高效能 l1 cache 一級快取 是cpu第一層快取記憶體,分為資料快取和指令快取,一般伺服器cpu的l1快取...