【轉載】關於CPU Cache -- 程序猿須要知道的那些事

文章歡迎轉載,但轉載時請保留本段文字,並置於文章的頂部
做者:盧鈞軼(cenalulu)
本文原文地址:http://cenalulu.github.io/linux/all-about-cpu-cache/html

先來看一張本文全部概念的一個思惟導圖linux

mind_map

爲何要有CPU Cache

隨着工藝的提高最近幾十年CPU的頻率不斷提高,而受制於製造工藝和成本限制,目前計算機的內存主要是DRAM而且在訪問速度上沒有質的突破。因 此,CPU的處理速度和內存的訪問速度差距愈來愈大,甚至能夠達到上萬倍。這種狀況下傳統的CPU經過FSB直連內存的方式顯然就會由於內存訪問的等待, 致使計算資源大量閒置,下降CPU總體吞吐量。同時又因爲內存數據訪問的熱點集中性,在CPU和內存之間用較爲快速而成本較高的SDRAM作一層緩存,就 顯得性價比極高了。git

爲何要有多級CPU Cache

隨着科技發展,熱點數據的體積愈來愈大,單純的增長一級緩存大小的性價比已經很低了。所以,就慢慢出現了在一級緩存(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)兩種專門用途的緩存。
下面一張圖能夠看出各級緩存之間的響應時間差距,以及內存到底有多慢!數組

latency

什麼是Cache Line

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的填充,因爲緩存填充的時間遠高於數據訪問的響應時間,所以多一次緩存填充對於總執行的影響會被放大,最終獲得下圖的結果:
cache_size
若是讀者有興趣的話也能夠在本身的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; } }

CPU Cache 是如何存放數據的

你會怎麼設計Cache的存放規則

咱們先來嘗試回答一下那麼這個問題:

假設咱們有一塊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的功能在成本上是很是高的。

爲何Cache不能作成Fully Associative

Fully Associative 字面意思是全關聯。在CPU Cache中的含義是:若是在一個Cache集內,任何一個內存地址的數據能夠被緩存在任何一個Cache Line裏,那麼咱們成這個cache是Fully Associative。從定義中咱們能夠得出這樣的結論:給到一個內存地址,要知道他是否存在於Cache中,須要遍歷全部Cache Line並比較緩存內容的內存地址。而Cache的本意就是爲了在儘量少得CPU Cycle內取到數據。那麼想要設計一個快速的Fully Associative的Cache幾乎是不可能的。

爲何Cache不能作成Direct Mapped

和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-Way Set Associative緩存就出現了。他的原理是把一個緩存按照N個Cache Line做爲一組(set),緩存按組劃爲等分。這樣一個64位系統的內存地址在4MB二級緩存中就劃成了三個部分(見下圖),低位6個bit表示在 Cache Line中的偏移量,中間12bit表示Cache組號(set index),剩餘的高位46bit就是內存地址的惟一id。這樣的設計相較前兩種設計有如下兩點好處:

  • 給定一個內存地址能夠惟一對應一個set,對於set中只需遍歷16個元素就能夠肯定對象是否在緩存中(Full Associative中比較次數隨內存大小線性增長)
  • 2^18(256K)*16(way)=4M的連續熱點數據纔會致使一個set內的conflict(Direct Mapped中512K的連續熱點數據就會出現conflict)

addr

爲何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 中的測試結果圖,來解釋下內存對齊在極端狀況下帶來的性能損失。
memory_align

該圖其實是咱們上文中第一個測試的一個變種。縱軸表示了測試對象數組的大小。橫軸表示了每次數組元素訪問之間的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的架構下會出現的問題能夠詳細閱讀如下兩篇文章:


Cache淘汰策略

在文章的最後咱們順帶提一下CPU Cache的淘汰策略。常見的淘汰策略主要有LRURandom兩種。一般意義下LRU對於Cache的命中率會比Random更好,因此CPU Cache的淘汰策略選擇的是LRU。固然也有些實驗顯示在Cache Size較大的時候Random策略會有更高的命中率

總結

CPU Cache對於程序猿是透明的,全部的操做和策略都在CPU內部完成。可是,瞭解和理解CPU Cache的設計、工做原理有利於咱們更好的利用CPU Cache,寫出更多對CPU Cache友好的程序

Reference

    1. Gallery of Processor Cache Effects
    2. How Misaligning Data Can Increase Performance 12x by Reducing Cache Misses
    3. Introduction to Caches
相關文章
相關標籤/搜索