CSAPP讀書筆記第六章(優化cache命中率)

(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];

        }
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章