CPU是計算機和核心,所有運算和程式執行都是由它負責。一般地,資料和程式都儲存在主記憶體(RAM)中。隨著技術和製作工藝的提升,CPU 的處理速度遠遠快於記憶體的訪問速度,其差距甚至可以達到上萬倍,如果CPU直接訪問記憶體,則會因為記憶體訪問的等待,導致計算資源大量閒置,降低 CPU 整體吞吐量。同時,根據區域性性訪問原理,記憶體資料訪問的熱點比較集中。因此誕生了CPU快取。 CPU 快取由更快的 SRAM 構成(記憶體是由 DRAM 構成的),而且離 CPU 核心更近,如果運算時需要的輸入資料是從 CPU 快取,而不是記憶體中讀取時,運算速度就會快很多。瞭解 CPU 快取對效能的影響,能夠更有效地編寫我們的程式碼,提升CPU命中率,最佳化程式效能。
當下的 CPU 都是多核心的,一般採用三級快取設計,其中所有核心共享三級快取,每個核心都有自己的一、二級快取,所以三級快取要比一、二級快取大許多倍。快取要比記憶體快很多,CPU 訪問一次記憶體通常需要 100 個時鐘週期以上,而訪問一級快取只需要 4~5 個時鐘週期,二級快取大約 12 個時鐘週期,三級快取大約 30 個時鐘週期。如果 CPU 所要操作的資料在快取中,則直接讀取,這稱為快取命中。命中快取會帶來很大的效能提升,因此,我們的程式碼最佳化目標是提升 CPU 快取的命中率。
CPU Cache line定義了一個二維陣列 long[][] array = new long[N][N];並且使用了橫向遍歷和縱向遍歷兩種順序對這個二位陣列進行遍歷,遍歷總次數相同,只不過迴圈的方向不同,那這兩種遍歷方式哪個更快呢?透過實際測試發現,當N足夠大時,橫向遍歷要比縱向遍歷快得多(8倍左右)。這就是cache line在起作用。
快取行 (Cache Line) 是 CPU Cache 中的最小單位,CPU Cache 由若干快取行組成,一個快取行的大小通常是 64 位元組。一個 Java 的 long 型別是 8 位元組,因此在一個快取行中可以存 8 個 long 型別的變數。二維陣列 array 所佔用的記憶體是連續的,如果橫向遍歷陣列元素,則完全與上述記憶體中元素順序一致,訪問陣列元素時,會一次性載入8個數組元素到快取中;而縱向遍歷,記憶體是跳躍訪問的,雖然每次也會載入8個數組元素,但其命中率只有八分之一,每訪問下一個元素,都需要重新載入8個數組元素。因此,我們測試結果有8倍左右差距。CPU 快取在順序訪問連續記憶體資料時揮發出了最大的優勢,遇到這種遍歷訪問陣列的情況時,按照記憶體佈局順序訪問將會帶來很大的效能提升。
偽共享偽共享指的是多個執行緒同時讀寫同一個快取行的不同變數時導致的 CPU 快取失效。以64位元組的快取行為例,記憶體中連續的64位元組都會被載入到快取行中,除了目標資料還會有其他資料。如下圖所示,假如變數X和變數Y在同一快取行中,Core1需要操作變數X,Core2需要操作變數Y。
Core1修改快取行內的變數X後,按照快取一致性協議,Core2中快取行將失效,Core1將最新快取行資料寫回記憶體。Core2需重新從記憶體中載入包含變數Y的快取行資料,並放置快取。如果Core2修改變數Y,需要Core1將快取行置為失效,Core2將最新快取寫回記憶體。Core1或其他處理器如需操作同一快取行內的其他資料,同上述步驟。不管哪個Core首先對變數做了修改,都會使得其它Core上整個 CacheLine 失效(因為 CacheLine 是 CPU 快取的最小單位),也就意味著,頻繁的多執行緒操作,CPU 快取將會徹底失效,降級為 CPU core 和主記憶體的直接互動。
偽共享問題的解決思路有也很典型:空間換時間,讓目標變數獨佔快取行,即快取行中只儲存目標變數。以64位元組的快取行為例,偽共享問題產生的前提是,併發情況下,不同cpu對快取行中不同變數的操作引起的。那麼,如果把快取行中僅儲存目標變數,其餘空間採用“無用”資料填充補齊64位元組,就不會產生偽共享問題。這種方式就是:快取行填充(也稱快取行對齊)。Nginx中使用雜湊表來存放域名、HTTP 頭部等資料的,而雜湊表裡桶的大小預設就等於 CPU Cache Line 的值,如果需要調整,Nginx 官網上也明確建議應該設定為 CPU Cache Line 的整數倍。就是因為按照 cpu cache line(比如 64 位元組)來訪問記憶體時,不會出現多核 CPU 下的偽共享問題,可以儘量減少訪問記憶體的次數。
分支預測器假設有如下需求:對於一個無序陣列,需要進行如下操作:①陣列中大於某個值得數全部置為0;②排序。那麼是先排序再查詢更快,還是先查詢再排序更快呢?實驗證明,先排序遠遠快於後排序。這就是分支預測器在起作用。
對於條件判斷,會有跳轉執行和不跳轉兩種選擇。對於CPU來說,執行條件判斷時,分為取指令和執行指令等階段。分支預測器可以根據規則,預測指令跳轉或者不跳轉,進而根據預測取指令。是否條件跳轉,只有在該分支指令在指令流水線中通過了執行階段才能確定下來;如果預測成功則執行;不成功則重新取指令。
分支預測器分為靜態預測器和動態預測器,它們對應著不同的預測規則。所謂靜態預測器,就是說統一全部跳轉或者全部不跳轉;而動態預測器會根據上次跳轉情況進行判斷,如果上次跳轉,則本次跳轉,否則不跳轉。
對於以上無序陣列的例子來說,當陣列中的元素完全隨機時,分支預測器無法有效工作;而當 array 陣列有序時,分支預測器會動態地根據歷史跳轉情況對未來進行預測,指令命中率就會非常高。