(1)局部性:
程序具有時間局部性和空間局部性.時間局部性是指當前用的存儲器位置可能在不久的將來被用到,會被放入告訴緩存。空間局部性則是指一個存儲器位置被用到,那麼相鄰的幾個位置在不久的將來也可能被用到,也會被放入告訴緩存!
根據我在vs2013的測試,發現sum函數要比sum2快幾十倍的速度,這完全得益於我們按照行來訪問,這天然的符合vector的存儲方式,另外在release情況下編譯器會優化掉a[i][j],它會找一個臨時變量 x = a[i],這樣就不需要每次都去尋找a[i]了。
data_type sum(vector<vector<data_type>>& a,int k ){
auto M = a.size(), N = a[0].size();
data_type res = 0;
for (size_t i = 0; i != M; ++i)
{
for (size_t j = 0; j != N; ++j)
res +=a[i][j];
}
return res+k;
}
data_type sum2(vector<vector<data_type>>& a,int k){
auto M = a.size(), N = a[0].size();
data_type res = 0;
for (size_t j = 0; j != N; ++j)
{
for (size_t i = 0; i != M; ++i)
res += a[i][j];
}
return res+k;
}
再看一個關於空間局部性的例子:
struct point {
int vec[3];
int acc[3];
};
using p_array = vector<point>;
可以發現如果clear函數沒有進行auto &p1 = p[i]的優化,編譯器居然沒有進行這樣的優化,其原因不得而知,所以最好的辦法是clear2這樣,簡單而且速度快。
void clear(p_array& p ){
int n = p.size();
for (int i = 0; i != n; ++i)
{
auto& p1 = p[i];//編譯器沒有對這裏進行優化
for (int j = 0; j != 3; ++j)
p1.vec[j] = 0;
for (int j = 0; j != 3; ++j)
p1.acc[j] = 0;
}
}
void clear2(p_array& p){
int n = p.size();
for (point& p1:p)
{
for (int j = 0; j != 3; ++j)
p1.vec[j] = 0;
for (int j = 0; j != 3; ++j)
p1.acc[j] = 0;
}
}
針對這種寫法:編譯器似乎會生成臨時變量p來代替p[i],不過這不能掩蓋空間局部性的不足。當然這樣的可讀性要好一點.這種做法比起clear2要慢了1倍。
void clear3(p_array& p){
int n = p.size();
for (int i = 0; i != n; ++i)
{
for (int j = 0; j != 3; ++j)
{
p[i].vec[j] = 0;
p[i].acc[j] = 0;
}
}
}
(2)高速緩存的命中
高速緩存是將由組,行,塊三個概念組成的,其中一行除了標誌位就是一個塊,塊在某一級緩存裏大小是固定的。每次不命中時都會從下級緩存讀取一個塊大小的數據拷貝到上級。這裏會利用地址的中間位進行一個hash確定是分配到哪些組,利用中間的位是爲了避免衝突不命中。OK,來看一看幾個基本的例子:
分析可知,我們第一次不命中的時候會將一個快都讀入告訴緩存,然而由於第一個循環直接跳過下一個字節,所以接下來的兩次讀入市命中,不命中然後反覆循環。同時由於數組一共的大小是8*256 = 2048Bytes,所以到了一半的時候會驅逐掉最開始緩存的行的數據,這樣第二個for循環又不能利用前面的緩存了(這裏發生了衝突不命中),所以第二個循環也是重頭開始。最終的不命中率是0.5.
按照同樣的方式分析即可:
矩陣乘法優化:
我們知道矩陣乘法的公式是:C[i][j]=A[i][k]∗B[k][j] ,那麼我們可以有多種實現循環的方式,最直接的寫法可能是i,j,k;但是如果使用循環i,k,j那麼運行的速度可能會快上幾十倍!這是因爲i,k,j的順序最符合局部性!每次都只會取下一個元素。
void vec1(vector<data_type>& a, vector<data_type>& b, vector<data_type>& c){
auto n = a.size();
double r;
for (size_t i = 0; i < n; ++i)
{
for (size_t k = 0; k < n; ++k)
{
r = a[i][k];
for (size_t j = 0; j < n; ++j)
{
c[i][j] += b[k][j] * r;
}
}
}
}
對矩陣轉置進行優化:
如果像下面這樣寫,寫不命中率可能是1!(如果數組太大的話,緩存不能夠存下)。這裏面難點在於讀寫是矛盾的。
void vec1(vector<data_type>& a, vector<data_type>& b){
auto n = a.size();
for (size_t i = 0; i < n; ++i)
{
for (size_t j = 0; j < n; ++j)
{
a[j][i] = b[i][j] ;
}
}
}
下面這個優化很巧妙:利用二層循環展開,這樣每次的寫不命中率最多隻有0.5。測試發現速度提高了一倍!!!
void vec2(vector<data_type>& dst, vector<data_type>& src){
auto n = dst.size();
double r;
for (size_t i = 0; i < n-2; i+=2)
{
for (size_t j = 0; j < n-2; j+=2)
{
dst[j][i] = src[i][j];
dst[j+1][i] = src[i][j+1];
dst[j][i+1] = src[i + 1][j];
dst[j+1][i+1] = src[i + 1][j+1];
}
}
}
加強版的優化 :只需要對i進行展開就可以了!對j展開不能提高命中率!(比原始方法快樂4倍…)
void vec2(vector<data_type>& dst, vector<data_type>& src){
auto n = dst.size();
double r;
for (size_t i = 0; i < n-3; i+=4)
{
for (size_t j = 0; j < n ; j += 1)
{
dst[j][i] = src[i][j];
dst[j][i+1] = src[i + 1][j];
dst[j][i + 2] = src[i + 2][j];
dst[j][i +3] = src[i + 3][j];
}
}
}