【底層原理】從快取來看區域性性提高程式執行效率的原因

神一樣的程式設計發表於2018-10-15
計算機儲存結構

在計算機系統中,儲存裝置都被組織成了一個儲存器層次結構,如下圖所示:

【底層原理】從快取來看區域性性提高程式執行效率的原因

我們發現,越往上,儲存器的容量越小、成本越高、速度越快。其實在最開始的時候,計算機儲存器層次只有三層:cpu暫存器、DRAM主存以及磁碟儲存。那為什麼後來搞得這麼複雜了?

這是因為CPU和主存之間存在巨大速度差異,作為核心的CPU處理資料的速度極快,記憶體跟不上,怎麼辦?系統設計者被迫在CPU暫存器和主存之間插入了一個小的SRAM快取記憶體儲存器稱為L1快取,大約可以在2--4個時鐘週期內訪問。再後來發現L1快取記憶體和主存之間還是有較大差距,又在L1快取記憶體和主存之間插入了速度稍微慢點的L2快取,大約可以在10個時鐘週期內訪問。於是,在這樣的模式下,在不斷的演變中形成了現在的儲存體系。


什麼是快取

儲存器層次結構的主要思想是上一層的儲存器作為低一層儲存器的快取記憶體。因此,暫存器L0就是L1的快取記憶體,L1是L2的快取記憶體,L2是L3的快取記憶體,L3是主存的快取記憶體,而主存又是磁碟的快取記憶體。

也就是說,對於每個k ,位於k層的更快更小的儲存器裝置作為第k+1層的更大更慢儲存裝置的快取。就是說,k層儲存了k+1層中經常被訪問的資料。在快取之間,資料是以塊為單位傳輸的。當然不同層次的快取,塊的大小會不同。一般來說是越往上,塊越小。

下圖就是一個示例:

【底層原理】從快取來看區域性性提高程式執行效率的原因

上圖中,k是k+1的快取,k中快取了k+1中塊編號為 4、9、14、3的資料。他們之間的資料傳輸是以塊大小為單位的。當程式需要這些塊中的資料時,可直接衝快取k中得到。這比從k+1層讀資料要快


快取命中和快取失效

下面以上圖為例再來解釋兩個概念:

快取命中:當程式需要第k+1層中的某個資料時d,會首先在它的快取k層中尋找。如果資料剛好在k層中,就稱為快取命中(cache hit),如在上圖中,若程式訪問k+1層的4,先去其快取層k去找,4恰好k層,發生了快取命中。

快取失效:快取失效也稱快取不命中,當需要的資料物件d不在快取k中時,稱為快取不命中。當發生快取不命中時,cpu會直接從k+1層取出包含資料物件d的那個塊,然後需要將其再快取到k層,以便下次再訪問時就能直接從快取層k中取到。

對於快取失效的資料被記憶體獲取後再存入到k層的快取中時,如果此時k層的快取已經放滿的話,就會覆蓋其中的一個塊。至於要覆蓋哪一個塊,這是有快取中的替換策略決定的,比如說在LRU快取(請戳我)一文中介紹的LRU快取就是一種替換策略。這裡不再討論。


程式區域性性如何影響程式效能

好了,現在我們可以說說為什麼區域性性好的程式能有更好的效能了。

利用時間區域性性:由於時間區域性性,同一個資料物件會多次被使用。一旦一個資料物件從k+1層進入到k層的快取中,就希望它多次被引用。這樣能節省很多訪問造成的時間開支。

利用空間區域性性:假設快取k能存n個資料塊。在對陣列訪問的時候,由於陣列是連續存放的,對第一個元素訪問的時候,會把第一個元素後面的一共n個元素(快取以塊為單位傳輸)拷貝到快取k中,這樣在對第二個元素到第n個元素的訪問時就可以直接從快取裡獲取,從而提高效能。

我們還是分析一個例子,在程式區域性性原理介紹(請戳我)一文中已經知道了下面兩個函式fun_1的效率幾乎比fun_2的效率高了一倍(不同機器執行結果可能不太一樣),現在我們來分析其原因。

//先訪問行
void fun_1()
{    
    int i,j;    
   
    for(i=0; i<500; i++)    
    {
	for(j=0; j<500; j++)
	{
	    a[i][j]=i;
	}    
    }
}

//先訪問列
void fun_2()
{    
    int i,j;  
    
    for(j=0; j<500; j++)    
    {
	for(i=0; i<500; i++)
	{
	    a[i][j]=i;
	}    
    }
}複製程式碼

我們知道,這兩個函式的區別在於fun_1函式是按行訪問,fun_2函式是按列訪問,正是這細微的不同導致效率上的差別。

我們先來看看按行訪問,發生了什麼,為了分析方便,我們假設:快取每次只能快取一塊,一塊大小隻能存放3個int型別資料,假設對於一個兩行三列的int[2][3]的陣列訪問。

按行訪問時,訪問a[0][0],時直接從記憶體讀取,然後將a[0][0]以及其後的兩個數快取到其快取中,這樣再訪問a[0][1],a[0][2]時直接從快取中讀取,對於第二行的訪問也是如此。因此對於整個陣列6個元素的訪問,只訪問了2次記憶體,快取命中了4次。

【底層原理】從快取來看區域性性提高程式執行效率的原因

再來看看按列訪問,當訪問a[0][0],時直接從記憶體讀取,然後將a[0][0]以及其後的兩個數快取到其快取中,看起來和按行訪問沒什麼區別,那好,再看下一步,按列訪問的話接下來應該是訪問a[1][0]了,先去快取中找,肯定找不到了,沒辦法,只能再次訪問記憶體。對於第2、3列情況一樣,這樣,同樣一個陣列,按列訪問訪問了6次記憶體,效率當然比按行訪問低了。

【底層原理】從快取來看區域性性提高程式執行效率的原因


相關文章