百度的一道面試題(關於Cache的)
某型CPU的一級資料快取大小為16K位元組,cache塊大小為64位元組;二級快取大小為256K位元組,cache塊大小為4K位元組,採用二路組相聯。經測試,下面兩段程式碼執行時效率差別很大,請分析哪段程式碼更好,以及可能的原因。
為了進一步提高效率,你還可以採取什麼辦法?
A段程式碼:
int matrix[1023][15];
const char *str = "this is a str";
int i, j, tmp, sum = 0;
tmp = strlen(str);
for(i = 0; i < 1023; i++)
for(j = 0; j < 15; j++)
sum += matrix[i][j] + tmp;
B段程式碼 :
int matrix[1025][17];
const char *str = "this is a str";
int i, j, sum = 0;
for(i = 0; i < 17; i++)
for(j = 0; j < 1025; j++)
sum += matrix[j][i] + strlen(str);
A段程式碼效率要遠遠高於B段程式碼,原因有三:
1、
B效率低最要命的地方就是每次都要呼叫strlen()函式,這是個嚴重問題,屬於邏輯級錯誤。假設A的兩層迴圈都不改變,僅僅是把A的那個迴圈裡面的temp換成strlen()呼叫,在Windows 2000 (Intel 雙) 下測試,竟然是A的執行時間的3.699倍。(這裡沒有涉及不同CPU有不同的Cache設計)僅僅是這一點就已經說明B段程式碼垃圾程式碼。
2、
這也是一個邏輯級的錯誤。在這裡我們再做個試驗,A、B段程式碼分別採用大小一樣的陣列[1023][15]、[1023][16]、[1023][17],只是在迴圈上採取了不同的方式。兩者在執行時間上也是有很大差異的了。B的執行時間大概是A的1.130倍。
那麼這是因為什麼呢?其實也很簡單,那就是A段程式碼中的迴圈執行語句對記憶體的訪問是連續的,而B段程式碼中的迴圈執行語句對記憶體的訪問是跳躍的。直接降低了B程式碼的執行效率。
這裡不是內層迴圈執行多少次的問題,而是一個對記憶體訪問是否連續的問題。
3、
A的二維陣列是[1023][15],B的二維陣列是[1027][17],在這裡B段程式碼有犯了一個CPU級錯誤(或者是Cache級的錯誤)。
因為在Cache中資料或指令是以行為單位儲存的(也可以說是Cache塊),一行又包含了很多字。如現在主流的設計是一行包含64Byte。每一行擁有一個Tag。因此,假設CPU需要一個標為Tag 1的行中的資料,它會通過CAM對Cache中的行進行查詢,一旦找到相同Tag的行,就對其中的資料進行讀取。
A的是15 *4B = 60B,一個Cache行剛好可以儲存。B的是17*4B = 68B,超過了一個Cache行所儲存的資料。很明顯17的時候命中率要低於15的時候。
現在我們先不管A、B的迴圈巢狀的順序,僅僅拿A段程式碼來做個試驗,我們將會分三種情況來進行:
[1023][15][1023][16][1023][17]
執行結果並沒有出乎意料之外 17 的時候的執行時間大概是 15 的時候的1.399倍,除去有因為17的時候多執行迴圈,17/15 = 1.133 。進行折算,17的時候大概是15的時候的1.265倍。
16的時候的執行時間要比15的時候的執行時間要短,因為是16的時候,Cache命中率更高。16/15 = 1.066 ,而15的執行時間卻是16的1.068倍,加上16多執行的消耗,進行折算,15的時候大概是16的時候執行時間的1.134倍。
因為A段程式碼是15,而B段程式碼是17,在這一點上B段程式碼的效率要低於A段程式碼的效率。這是一個CPU級的錯誤(或者是Cache級的錯誤),這裡涉及到Cache的塊大小,也就涉及到Cache命中率,也就影響到程式碼效率。
不再假設什麼,僅僅對A段和B段程式碼進行測試,B段程式碼的執行效率將是A段程式碼執行效率的3.95倍。當然最大的罪魁禍首就是B中的重複呼叫strlen()函式。後面兩個錯誤告訴我們當需要對大量資料訪問的時候,一定要注意對記憶體的訪問要儘量是連續而且迴圈內層的訪問接近Cache的塊大小,以提高Cache的命中率,從而提高程式的執行效率。
所以可以對程式碼進行一下修改:
#define XX15
#define YY1023
int matrix[XX][YY];
const char *str = "this is a str";
int i, j, tmp, sum = 0;
tmp = strlen(str);
for(i = 0; i < XX; i++)
for(j = 0; j < YY; j++)
sum += matrix[i][j] + tmp;
這個程式僅僅是把陣列的宣告給顛倒了一下,迴圈也顛倒了一下,看起來和執行起來和上面給出的A段程式碼沒有多大的區別。但是如果當XX很小,比如:8,那麼這段程式和給出的A段程式碼就有區別了。這是因為這樣做可以提高Cache的命中率。