08.27 對C語言程序中二維數組賦值時,為什麼改變順序效率就降低了?

學習C語言最有效的方法就是多做實驗,很多初學者深知這一點。小明在學到二維數組時,嘗試寫了一段給二維數組賦值的代碼,他發現一個奇怪的現象:

交換賦值順序,效率是不同的。

對C語言程序中二維數組賦值時,為什麼改變順序效率就降低了?

交換賦值順序,效率是不同的

請看下面這兩段C語言代碼:

版本 1

int test1 ()
{
int i,j;
static int x[4000][4000];
for (i = 0; i < 4000; i++) {
for (j = 0; j < 4000; j++) {
x[j][i] = i + j;
}
}
return 0;
}

版本 2

int test2 ()
{
int i,j;
static int x[4000][4000];
for (j = 0; j < 4000; j++) {
for (i = 0; i < 4000; i++) {
x[j][i] = i + j;
}
}
return 0;
}
對C語言程序中二維數組賦值時,為什麼改變順序效率就降低了?

唯一的不同點在於交換了 i 和 j 變量

這兩個版本的C語言代碼幾乎完全相同,唯一的不同點在於交換了 i 和 j 變量,但是編譯成C語言程序執行後,消耗的時間卻是不同的,這是怎麼回事呢?

解析

可能有讀者認為這兩段C語言代碼產生效率上的差異,是因為編譯器處理這兩段代碼時,產生的指令不同。其實不是的,這兩段“對稱”的C語言代碼產生的指令是一致的,請看:

對C語言程序中二維數組賦值時,為什麼改變順序效率就降低了?

test1() 函數對應的彙編代碼

上圖是 test1() 函數對應的彙編代碼,下圖是 test2() 函數對應的彙編代碼。

對C語言程序中二維數組賦值時,為什麼改變順序效率就降低了?

test2() 函數對應的彙編代碼

仔細對比 test1() 和 test2() 對應的指令,我們很難發現二者有什麼不同。事實上,二者運行的效率差異主要來自於“緩存命中率”。簡單來說,就是連續操作C語言中的多維數組的最後一個維度最快,因此 test1() 中的賦值操作幾乎每次都會錯過緩存,而 test2() 中的賦值操作緩存命中率更高一些,因此 test2() 執行所消耗的時間更少。

在很多C語言初學者的腦海裡,二維數組各個元素在內存中的分佈可能是下面這樣的:

對C語言程序中二維數組賦值時,為什麼改變順序效率就降低了?

二維數組各個元素在內存中的分佈

但是計算機中的內存地址始終是一維的,因此在計算機眼中,二維數組仍然是按照一維排列的:

對C語言程序中二維數組賦值時,為什麼改變順序效率就降低了?

二維數組仍然是按照一維排列的

test2() 中的賦值操作先遍歷第二維,這一過程是下圖這樣的:

對C語言程序中二維數組賦值時,為什麼改變順序效率就降低了?

這一過程是下圖這樣的

此時C語言程序每次訪問數組元素,在內存中都是順序進行的,這對於緩存命中率的提升很有幫助。再來看 test1() 中的賦值操作,它優先遍歷第一維,因此訪問數組元素的過程是下圖這樣的:

對C語言程序中二維數組賦值時,為什麼改變順序效率就降低了?

訪問數組元素的過程是下圖這樣的

顯然,此時C語言程序訪問數組元素在內存中是“跳躍式”的,但是,計算機訪問內存各個地址的效率不是都一樣的嗎?那為什麼 test1() 和 test2() 的效率不同呢?

答案就是 test1() 和 test2() 的緩存命中率不同。計算機 CPU 一般都有更加高速的緩存(稱為“緩存線(cache line)”,常是 64 字節),訪問這一緩存的速度比訪問內存的速度要高的多(讀者可以對比想象計算機訪問內存的速度比訪問硬盤數據的速度快得多)。

小明的C語言代碼中賦值的元素是 int 型的(常常是 4 字節),因此 64 字節的緩存線可以容納 16 個連續的整數,CPU 訪問這 16 個數據要比訪問內存裡的 16 個數據快得多,並且 CPU 在加載緩存線數據的時間內,能處理相當多的工作。

對C語言程序中二維數組賦值時,為什麼改變順序效率就降低了?

CPU 在加載緩存線數據的時間內,能處理相當多的工作

CPU 在處理 test2() 中的賦值時,可以獲取 16 個連續的整數塊,然後賦值給數組,重複 4000*4000/16次,這樣的操作在緩存線的支持下相當快,因為 CPU 沒有被閒置,總是在處理事務。

再考慮 test1() 中的賦值,緩存線加載了 16 個整數塊,但是接著值修改了其中一個整數,因此它需要重複 4000*4000 次,相比於 test2() 中的賦值,需要 16 倍的內存“回遷”次數。而 CPU 實際上需要花時間坐著乾等內存操作完成,因此 test1() 的效率要低一些。

小結

本節主要討論了一個看似“靈異”的C語言二維數組賦值問題,這無關指令差異,更多的是緩存命中差異帶來的效率差異。但是讀者應該明白,並不是所有的計算機程序都如此,例如 Fortran 語言中,test1() 中的賦值效率要高於 test2() 中的賦值效率,因為它將二維數組在內存中展開時,是按照“列”優先排列的(C語言是按“行”優先排列的)。

對C語言程序中二維數組賦值時,為什麼改變順序效率就降低了?

歡迎在評論區一起討論,質疑。文章都是手打原創,每天最淺顯的介紹C語言、linux等嵌入式開發,喜歡我的文章就關注一波吧,可以看到最新更新和之前的文章哦。



分享到:


相關文章: