原來 CPU 為程式效能最佳化做了這麼多

本文主要來學習記憶體屏障和 CPU 快取知識,以便於我們去了解 CPU 對程式效能最佳化做了哪些努力。

首先來看下 CPU 快取:

CPU 快取

CPU 快取是為了提高程式執行的效能,CPU 在很多處理上內部架構做了很多調整,比如 CPU 快取記憶體,大家都知道因為硬碟很慢,可以透過快取把資料載入到記憶體裡面,提高訪問速度,而 CPU 處理也有這個機制,儘可能把處理器訪問主記憶體時間開銷放在 CPU 快取記憶體上面,CPU 訪問速度相比記憶體訪問速度又要快好多倍,這就是目前大多數處理器都會去利用的機制,利用處理器的快取以提高效能。

原來 CPU 為程式效能最佳化做了這麼多

多級快取

CPU 的快取分為三級快取,所以說多核 CPU 會有多個快取,我們首先來看下一級快取(L1 Cache):

L1 Cache 是 CPU 第一層快取記憶體,分為資料快取和指令快取,一般伺服器 CPU 的 L1 快取的容量通常在 32-4096 KB。由於 L1 級快取記憶體容量的限制,為了再次提高 CPU 的運算速度,在 CPU 外部放置-高速儲存器,即二級快取(L2 Cache)。

因為 L1 和 L2 的容量還是有限,因此提出了三級快取,L3 現在的都是內建的,它的實際作用即是,L3 快取的應用可以進一步降低記憶體延遲,同時提升大資料量計算時處理器的效能,具有較大 L3 快取的處理器提供更有效的檔案系統快取行為及較短訊息和處理器佇列長度,一般是多核共享一個 L3 快取。

CPU 在讀取資料時,先在 L1 Cache 中尋找,再從 L2 Cache 尋找,再從 L3 Cache 尋找,然後是記憶體,再後是外儲存器硬碟尋找。

如下圖所示,CPU 快取架構中,快取層級越接近 CPU core,容量越小,速度越快。CPU Cache 由若干快取行組成,快取行是 CPU Cache 中的最小單位,一個快取行的大小通常是 64 位元組,是 2 的倍數,不同的機器上為 32 到 64 位元組不等,並且它有效地引用主記憶體中的一塊地址。

原來 CPU 為程式效能最佳化做了這麼多

多 CPU 讀取同樣的資料進行快取,進行不同運算之後,最終寫入主記憶體以哪個 CPU 為準?這就需要快取同步協議了:

快取同步協議

在這種快取記憶體回寫的場景下,有很多 CPU 廠商提出了一些公共的協議-

MESI 協議

,它規定每條快取有個狀態位,同時定義了下面四個狀態:

修改態(Modified)

:此 cache 行已被修改過(髒行),內容已不同於主存,為此 cache 專有;

專有態(Exclusive)

:此 cache 行內容同於主存,但不出現於其它 cache 中;

共享態(Shared)

:此 cache 行內容同於主存,但也出現於其它 cache 中;

無效態(Invalid)

:此 cache 行內容無效(空行)。

多處理器,單個 CPU 對快取中資料進行了改動,需要通知給其它 CPU,也就是意味著,CPU 處理要控制自己的讀寫操作,還要監聽其他 CPU 發出的通知,從而保證

最終一致

執行時的指令重排

CPU 對效能的最佳化除了快取之外還有

執行時指令重排

,大家可以透過下面的圖瞭解下:

原來 CPU 為程式效能最佳化做了這麼多

比如圖中有程式碼 x = 10;y = z;,這個程式碼的正常執行順序應該是先將 10 寫入 x,讀取 z 的值,然後將 z 值寫入 y,實際上真實執行步驟,CPU 執行的時候可能是先讀取 z 的值,將 z 值寫入 y,最後再將 10 寫入 x,為什麼要做這些修改呢?

因為當 CPU 寫快取時發現快取區正被其他 CPU 佔用(例如:三級快取),為了提高 CPU 處理效能,可能將後面的

讀快取命令優先執行

指令重排並非隨便重排,是需要遵守 as-if-serial 語義的,as-if-serial 語義的意思是指不管怎麼重排序(編譯器和處理器為了提高並行度),單執行緒程式的執行結果不能被改變。編譯器,runtime 和處理器都必須遵守 as-if-serial 語義,也就是說編譯器和處理器

不會對存在資料依賴關係的操作做重排序

那麼這樣就會有如下兩個問題:

CPU 快取記憶體下有一個問題:

快取中的資料與主記憶體的資料並不是實時同步的,各 CPU(或 CPU 核心)間快取的資料也不是實時同步。

在同一個時間點,各 CPU 所看到同一記憶體地址的資料的值可能是不一致的

CPU 執行指令重排序最佳化下有一個問題:

雖然遵守了 as-if-serial語義,僅在單 CPU 自己執行的情況下能保證結果正確。多核多執行緒中,指令邏輯無法分辨因果關聯,可能出現

亂序執行

,導致程式執行結果錯誤。

如何解決上述的兩個問題呢,這就需要談到

記憶體屏障

記憶體屏障

處理器提供了兩個

記憶體屏障(Memory Barrier)

指令用於解決上述兩個問題:

寫記憶體屏障(Store Memory Barrier)

:在指令後插入 Store Barrier,能讓寫入快取中的最新資料更新寫入主記憶體,讓其他執行緒可見。強制寫入主記憶體,這種顯示呼叫,CPU 就不會因為效能考慮而去對指令重排。

讀記憶體屏障(Load Memory Barrier)

:在指令前插入 Load Barrier,可以讓快取記憶體中的資料失效,強制從新的主記憶體載入資料。強制讀取主記憶體內容,讓 CPU 快取與主記憶體保持一致,避免了快取導致的一致性問題。

Java 中也有類似的機制,比如 Synchronized 和 volatile 都採用了記憶體屏障的原理。

總結

本文主要介紹了在提高程式執行效能上,CPU 作出了哪些最佳化:快取和執行時指令重排,最後還介紹了記憶體屏障相關知識。

想了解更多精彩內容,快來關注計算機java程式設計