原來 CPU 為程序性能優化做了這麼多

  • 2020 年 3 月 29 日
  • 筆記

本文主要來學習內存屏障和 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)。

因為 L1L2 的容量還是有限,因此提出了三級緩存,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 對性能的優化除了緩存之外還有運行時指令重排,大家可以通過下面的圖了解下:

比如圖中有代碼 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 語義,也就是說編譯器和處理器不會對存在數據依賴關係的操作做重排序

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

  1. CPU 高速緩存下有一個問題:

緩存中的數據與主內存的數據並不是實時同步的,各 CPU(或 CPU 核心)間緩存的數據也不是實時同步。在同一個時間點,各 CPU 所看到同一內存地址的數據的值可能是不一致的

  1. CPU 執行指令重排序優化下有一個問題:

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

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

內存屏障

處理器提供了兩個內存屏障(Memory Barrier) 指令用於解決上述兩個問題:

寫內存屏障(Store Memory Barrier):在指令後插入 Store Barrier,能讓寫入緩存中的最新數據更新寫入主內存,讓其他線程可見。強制寫入主內存,這種顯示調用,CPU 就不會因為性能考慮而去對指令重排。

讀內存屏障(Load Memory Barrier):在指令前插入 Load Barrier,可以讓高速緩存中的數據失效,強制從新的主內存加載數據。強制讀取主內存內容,讓 CPU 緩存與主內存保持一致,避免了緩存導致的一致性問題。

Java 中也有類似的機制,比如 Synchronizedvolatile 都採用了內存屏障的原理。

總結

本文主要介紹了在提高程序運行性能上,CPU 作出了哪些優化:緩存和運行時指令重排,最後還介紹了內存屏障相關知識。

參考

http://dwz.win/7ps