前言
上次做計算機系統的實驗,做到一個高速緩存的實驗,不過實驗內容比較簡單,就是驗證一下不同的數據存取方式對緩存命中率的影響(通過運行時間體現)
因爲我上課全程摸魚,所以今天來回顧一下高速緩存這個內容,加深映像(
高速緩存介紹
什麼是高速緩存
高速緩存是介於cpu和內存之間的一級存儲器。訪問cpu高速緩存的速度要快於訪問內存,因此高速緩存常常用來加速cpu的運算。
你可以在任何一本介紹計算機基礎的課本上找到類似的圖(圖片源 百度圖片)來描述計算機的各種存儲器:
高速緩存基本原理
cpu的速度遠遠快於內存,因此如果cpu只是從內存中讀取數據,那麼會花費較多的時間在等待數據上,我們希望有一種方法解決【從內存中讀數據慢】的問題,於是有了高速緩存。
對於數據的讀取基於兩個猜想,假設我們讀取內存地址x處的數據,那麼有
- 內存地址x處的數據在短時間內容易被再次訪問
- 內存地址x周圍的數據在短時間內容易被再次訪問
以上兩個猜想很容易得到證實,(這也是程序的局部性原理的一部分),比如我們會經常編寫這樣的代碼
// 猜想1: 頻繁地訪問指針a指向的地址存儲的變量
int* a = new int;
*a = 123;
for(int i=0; i<n; i++)
{
*a += i;
}
// 猜想2: 矩陣運算的時候 頻繁訪問相鄰的變量 a[i+1]
int a[10];
int b[10];
for(int i=0; i<9; i++)
{
a[i] += b[i+1];
a[i+1] += b[i];
}
頻繁訪問內存意味着速度慢。高速緩存的引入解決了這個問題
hit
我們在訪問一個內存地址的時候,先查看高速緩存裏面有無該地址的數據。如果有就直接從高速緩存中讀取數據,這個行爲我們叫做hit,即緩存命中
miss
如果高速緩存中沒有數據,我們需要從內存中讀取數據,這個行爲叫做miss。值得注意的是,我們一次讀取不是讀取一個內存單元的數據,而是讀取一個【cache行】的數據,這意味着目標地址及其附近區域的一些數據會被讀到高速緩存中,如果下次訪問鄰近的數據,就不會miss。(cache行一般是64字節)
圖解
下面通過一張圖解釋高速緩存的基本原理,這個原理告訴我們,對內存中的數據的訪問,數據們相距的越近,數據越集中,存取速度越快。
更加詳細
在《深入理解計算機系統》書中,給出了相當詳細的對於高速緩存的解釋,我們可以來一探究竟
簡單的來說,就是取數據的時候,先查看數據地址,然後做一次硬件哈希映射,查找緩存裏是否命中,然後按照情況做對應的操作。
簡單實驗驗證
我們模擬簡單的矩陣乘法,編寫兩個子程序,計算res=a*b
的矩陣乘法,我們使用一維數組來模擬n*n
大小的矩陣
其中1是正常計算,即按行訪問a矩陣的元素,然後按列訪問b矩陣的元素,最後對應位置相乘,寫入res矩陣。
void matrix_multiply_1(int* m1, int* m2, int* res, int n)
{
for(int i=0; i<n; i++)
for(int j=0; j<n; j++)
for(int k=0; k<n; k++)
res[i*n+j] += m1[i*n+k]*m2[i*k+j];
}
我們將1中的循環順序替換一下,由ijk
變爲ikj
,性能大提升!
void matrix_multiply_2(int* m1, int* m2, int* res, int n)
{
for(int i=0; i<n; i++)
for(int k=0; k<n; k++)
for(int j=0; j<n; j++)
res[i*n+j] += m1[i*n+k]*m2[i*k+j];
}
可以看到在3000x3000的規模下 1用時174s,而2用時僅僅115s
你可能覺得總的循環次數不變,爲何速度相差這麼大?
如果k在最內層,變化頻次最高,這意味着大量相隔很遠的數據(因爲以一維數組形式模擬矩陣在內存中的存放)將會被頻繁讀入cache中,而cache行存儲的是鄰近的幾個數據。
後一次訪問的地址,不在前一次訪問帶來的cache存儲的範圍內,當然miss率高
下面兩幅圖對比了兩種存取方式,黃色方塊是我們矩陣乘法訪問的元素,紅色方塊是訪問目標元素時帶來的cache行存儲。
方式1:每一列都要miss
方式2:每讀取完一個cache行,僅僅在首元素髮生miss,其他元素均隨着首元素的讀入而進入cache
代碼
#include <bits/stdc++.h>
using namespace std;
void matrix_multiply_1(int* m1, int* m2, int* res, int n)
{
for(int i=0; i<n; i++)
for(int j=0; j<n; j++)
for(int k=0; k<n; k++)
res[i*n+j] += m1[i*n+k]*m2[i*k+j];
}
void matrix_multiply_2(int* m1, int* m2, int* res, int n)
{
for(int i=0; i<n; i++)
for(int k=0; k<n; k++)
for(int j=0; j<n; j++)
res[i*n+j] += m1[i*n+k]*m2[i*k+j];
}
int main()
{
double t1, t2;
clock_t st, ed;
int n;
cin>>n;
int* m1 = new int[n*n];
int* m2 = new int[n*n];
int* res = new int[n*n];
for(int i=0; i<n; i++)
for(int j=0; j<n; j++)
m1[i*n+j]=rand()%1000;
for(int i=0; i<n; i++)
for(int j=0; j<n; j++)
m2[i*n+j]=rand()%1000;
memset(res, 0, sizeof(res));
st = clock();
matrix_multiply_1(m1, m2, res, n);
ed = clock();
t1 = (double)(ed-st) / CLOCKS_PER_SEC;
memset(res, 0, sizeof(res));
st = clock();
matrix_multiply_2(m1, m2, res, n);
ed = clock();
t2 = (double)(ed-st) / CLOCKS_PER_SEC;
cout<<t1<<endl<<t2<<endl;
return 0;
}