文章歡迎轉載,但轉載時請保留本段文字,並置於文章的頂部
做者:盧鈞軼(cenalulu)
本文原文地址:http://cenalulu.github.io/linux/all-about-cpu-cache/html
先來看一張本文全部概念的一個思惟導圖linux
隨着工藝的提高最近幾十年CPU的頻率不斷提高,而受制於製造工藝和成本限制,目前計算機的內存主要是DRAM而且在訪問速度上沒有質的突破。因 此,CPU的處理速度和內存的訪問速度差距愈來愈大,甚至能夠達到上萬倍。這種狀況下傳統的CPU經過FSB直連內存的方式顯然就會由於內存訪問的等待, 致使計算資源大量閒置,下降CPU總體吞吐量。同時又因爲內存數據訪問的熱點集中性,在CPU和內存之間用較爲快速而成本較高的SDRAM作一層緩存,就 顯得性價比極高了。git
隨着科技發展,熱點數據的體積愈來愈大,單純的增長一級緩存大小的性價比已經很低了。所以,就慢慢出現了在一級緩存(L1 Cache)和內存之間又增長一層訪問速度和成本都介於二者之間的二級緩存(L2 Cache)。下面是一段從What Every Programmer Should Know About Memory中摘錄的解釋:github
Soon after the introduction of the cache the system got more complicated. The speed difference between the cache and the main memory increased again, to a point that another level of cache was added, bigger and slower than the first-level cache. Only increasing the size of the first-level cache was not an option for economical rea- sons.設計模式
此外,又因爲程序指令和程序數據的行爲和熱點分佈差別很大,所以L1 Cache也被劃分紅L1i (i for instruction)和L1d (d for data)兩種專門用途的緩存。
下面一張圖能夠看出各級緩存之間的響應時間差距,以及內存到底有多慢!數組
Cache Line能夠簡單的理解爲CPU Cache中的最小緩存單位。目前主流的CPU Cache的Cache Line大小都是64Bytes。假設咱們有一個512字節的一級緩存,那麼按照64B的緩存單位大小來算,這個一級緩存所能存放的緩存個數就是512/64 = 8
個。具體參見下圖:緩存
爲了更好的瞭解Cache Line,咱們還能夠在本身的電腦上作下面這個有趣的實驗。架構
下面這段C代碼,會從命令行接收一個參數做爲數組的大小建立一個數量爲N的int數組。並依次循環的從這個數組中進行數組內容訪問,循環10億次。最終輸出數組總大小和對應總執行時間。app
#include "stdio.h" #include <stdlib.h> #include <sys/time.h> long timediff(clock_t t1, clock_t t2) { long elapsed; elapsed = ((double)t2 - t1) / CLOCKS_PER_SEC * 1000; return elapsed; } int main(int argc, char *argv[]) #******* { int array_size=atoi(argv[1]); int repeat_times = 1000000000; long array[array_size]; for(int i=0; i<array_size; i++){ array[i] = 0; } int j=0; int k=0; int c=0; clock_t start=clock(); while(j++<repeat_times){ if(k==array_size){ k=0; } c = array[k++]; } clock_t end =clock(); printf("%lu\n", timediff(start,end)); return 0; }
若是咱們把這些數據作成折線圖後就會發現:總執行時間在數組大小超過64Bytes時有較爲明顯的拐點(固然,因爲博主是在本身的Mac筆記本上測 試的,會受到不少其餘程序的干擾,所以會有波動)。緣由是當數組小於64Bytes時數組極有可能落在一條Cache Line內,而一個元素的訪問就會使得整條Cache Line被填充,於是值得後面的若干個元素受益於緩存帶來的加速。而當數組大於64Bytes時,必然至少須要兩條Cache Line,繼而在循環訪問時會出現兩次Cache Line的填充,因爲緩存填充的時間遠高於數據訪問的響應時間,所以多一次緩存填充對於總執行的影響會被放大,最終獲得下圖的結果:
若是讀者有興趣的話也能夠在本身的linux或者MAC上經過gcc cache_line_size.c -o cache_line_size
編譯,並經過./cache_line_size
執行。dom
瞭解Cache Line的概念對咱們程序猿有什麼幫助?
咱們來看下面這個C語言中經常使用的循環優化例子
下面兩段代碼中,第一段代碼在C語言中老是比第二段代碼的執行速度要快。具體的緣由相信你仔細閱讀了Cache Line的介紹後就很容易理解了。
for(int i = 0; i < n; i++) { for(int j = 0; j < n; j++) { int num; //code arr[i][j] = num; } }
for(int i = 0; i < n; i++) { for(int j = 0; j < n; j++) { int num; //code arr[j][i] = num; } }
咱們先來嘗試回答一下那麼這個問題:
假設咱們有一塊4MB的區域用於緩存,每一個緩存對象的惟一標識是它所在的物理內存地址。每一個緩存對象大小是64Bytes,全部能夠被緩存對象的大小總和(即物理內存總大小)爲4GB。那麼咱們該如何設計這個緩存?
若是你和博主同樣是一個大學沒有好好學習基礎/數字電路的人 的話,會以爲最靠譜的的一種方式就是:Hash表。把Cache設計成一個Hash數組。內存地址的Hash值做爲數組的Index,緩存對象的值做爲數 組的Value。每次存取時,都把地址作一次Hash而後找到Cache中對應的位置操做便可。
這樣的設計方式在高等語言中很常見,也顯然很高效。由於Hash值得計算雖然耗時(10000個CPU Cycle左右), 可是相比程序中其餘操做(上百萬的CPU Cycle)來講能夠忽略不計。而對於CPU Cache來講,原本其設計目標就是在幾十CPU Cycle內獲取到數據。若是訪問效率是百萬Cycle這個等級的話,還不如到Memory直接獲取數據。固然,更重要的緣由是在硬件上要實現 Memory Address Hash的功能在成本上是很是高的。
Fully Associative 字面意思是全關聯。在CPU Cache中的含義是:若是在一個Cache集內,任何一個內存地址的數據能夠被緩存在任何一個Cache Line裏,那麼咱們成這個cache是Fully Associative。從定義中咱們能夠得出這樣的結論:給到一個內存地址,要知道他是否存在於Cache中,須要遍歷全部Cache Line並比較緩存內容的內存地址。而Cache的本意就是爲了在儘量少得CPU Cycle內取到數據。那麼想要設計一個快速的Fully Associative的Cache幾乎是不可能的。
和Fully Associative徹底相反,使用Direct Mapped模式的Cache給定一個內存地址,就惟一肯定了一條Cache Line。設計複雜度低且速度快。那麼爲何Cache不使用這種模式呢?讓咱們來想象這麼一種狀況:一個擁有1M L2 Cache的32位CPU,每條Cache Line的大小爲64Bytes。那麼整個L2Cache被劃爲了1M/64=16384
條Cache Line。咱們爲每條Cache Line從0開始編上號。同時32位CPU所能管理的內存地址範圍是2^32=4G
,那麼Direct Mapped模式下,內存也被劃爲4G/16384=256K
的 小份。也就是說每256K的內存地址共享一條Cache Line。可是,這種模式下每條Cache Line的使用率若是要作到接近100%,就須要操做系統對於內存的分配和訪問在地址上也是近乎平均的。而與咱們的意願相反,爲了減小內存碎片和實現便 捷,操做系統更多的是連續集中的使用內存。這樣會出現的狀況就是0-1000號這樣的低編號Cache Line因爲內存常常被分配並使用,而16000號以上的Cache Line因爲內存鮮有進程訪問,幾乎一直處於空閒狀態。這種狀況下,原本就寶貴的1M二級CPU緩存,使用率也許50%都沒法達到。
爲了不以上兩種設計模式的缺陷,N-Way Set Associative緩存就出現了。他的原理是把一個緩存按照N個Cache Line做爲一組(set),緩存按組劃爲等分。這樣一個64位系統的內存地址在4MB二級緩存中就劃成了三個部分(見下圖),低位6個bit表示在 Cache Line中的偏移量,中間12bit表示Cache組號(set index),剩餘的高位46bit就是內存地址的惟一id。這樣的設計相較前兩種設計有如下兩點好處:
2^18(256K)*16(way)
=4M
的連續熱點數據纔會致使一個set內的conflict(Direct Mapped中512K的連續熱點數據就會出現conflict)爲何N-Way Set Associative的Set段是從低位而不是高位開始的
下面是一段從How Misaligning Data Can Increase Performance 12x by Reducing Cache Misses摘錄的解釋:
The vast majority of accesses are close together, so moving the set index bits upwards would cause more conflict misses. You might be able to get away with a hash function that isn’t simply the least significant bits, but most proposed schemes hurt about as much as they help while adding extra complexity.
因爲內存的訪問一般是大片連續的,或者是由於在同一程序中而致使地址接近的(即這些內存地址的高位都是同樣的)。因此若是把內存地址的高位做爲 set index的話,那麼短期的大量內存訪問都會由於set index相同而落在同一個set index中,從而致使cache conflicts使得L2, L3 Cache的命中率低下,影響程序的總體執行效率。
瞭解N-Way Set Associative的存儲模式對咱們有什麼幫助
瞭解N-Way Set的概念後,咱們不可貴出如下結論:2^(6Bits <Cache Line Offset> + 12Bits <Set Index>)
= 2^18
= 256K
。 即在連續的內存地址中每256K都會出現一個處於同一個Cache Set中的緩存對象。也就是說這些對象都會爭搶一個僅有16個空位的緩存池(16-Way Set)。而若是咱們在程序中又使用了所謂優化神器的「內存對齊」的時候,這種爭搶就會愈加增多。效率上的損失也會變得很是明顯。具體的實際測試咱們能夠 參考: How Misaligning Data Can Increase Performance 12x by Reducing Cache Misses 一文。
這裏咱們引用一張Gallery of Processor Cache Effects 中的測試結果圖,來解釋下內存對齊在極端狀況下帶來的性能損失。
該圖其實是咱們上文中第一個測試的一個變種。縱軸表示了測試對象數組的大小。橫軸表示了每次數組元素訪問之間的index間隔。而圖中的顏色表示 了響應時間的長短,藍色越明顯的部分表示響應時間越長。從這個圖咱們能夠獲得不少結論。固然這裏咱們只對內存帶來的性能損失感興趣。有興趣的讀者也能夠閱 讀原文分析理解其餘從圖中能夠獲得的結論。
從圖中咱們不難看出圖中每1024個步進,即每1024*4
即4096Bytes,都有一條特別明顯的藍色豎線。也就是 說,只要咱們按照4K的步進去訪問內存(內存根據4K對齊),不管熱點數據多大它的實際效率都是很是低的!按照咱們上文的分析,若是4KB的內存對齊,那 麼一個240MB的數組就含有61440個能夠被訪問到的數組元素;而對於一個每256K就會有set衝突的16Way二級緩存,總共有256K/4K
=64
個元素要去爭搶16個空位,總共有61440/64
=960
個這樣的元素。那麼緩存命中率只有1%,天然效率也就低了。
除了這個例子,有興趣的讀者還能夠查閱另外一篇國人對Page Align致使效率低的實驗:http://evol128.is-programmer.com/posts/35453.html
想要知道更多關於內存地址對齊在目前的這種CPU-Cache的架構下會出現的問題能夠詳細閱讀如下兩篇文章:
在文章的最後咱們順帶提一下CPU Cache的淘汰策略。常見的淘汰策略主要有LRU
和Random
兩種。一般意義下LRU對於Cache的命中率會比Random更好,因此CPU Cache的淘汰策略選擇的是LRU
。固然也有些實驗顯示在Cache Size較大的時候Random策略會有更高的命中率
CPU Cache對於程序猿是透明的,全部的操做和策略都在CPU內部完成。可是,瞭解和理解CPU Cache的設計、工做原理有利於咱們更好的利用CPU Cache,寫出更多對CPU Cache友好的程序