(C語言記憶體五)C語言記憶體對齊,提高定址效率
計算機記憶體是以位元組(Byte)為單位劃分的,理論上CPU可以訪問任意編號的位元組,但實際情況並非如此。
CPU 通過地址匯流排來訪問記憶體,一次能處理幾個位元組的資料,就命令地址匯流排讀取幾個位元組的資料。32 位的 CPU 一次可以處理4個位元組的資料,那麼每次就從記憶體讀取4個位元組的資料;少了浪費主頻,多了沒有用。64位的處理器也是這個道理,每次讀取8個位元組。
以32位的CPU為例,實際定址的步長為4個位元組,也就是隻對編號為 4 的倍數的記憶體定址,例如 0、4、8、12、1000 等,而不會對編號為 1、3、11、1001 的記憶體定址。如下圖所示:
這樣做可以以最快的速度定址:不遺漏一個位元組,也不重複對一個位元組定址。
對於程式來說,一個變數最好位於一個定址步長的範圍內,這樣一次就可以讀取到變數的值;如果跨步長儲存,就需要讀取兩次,然後再拼接資料,效率顯然降低了。
例如一個 int 型別的資料,如果地址為 8,那麼很好辦,對編號為 8 的記憶體定址一次就可以。如果編號為 10,就比較麻煩,CPU需要先對編號為 8 的記憶體定址,讀取4個位元組,得到該資料的前半部分,然後再對編號為 12 的記憶體定址,讀取4個位元組,得到該資料的後半部分,再將這兩部分拼接起來,才能取得資料的值。
將一個數據儘量放在一個步長之內,避免跨步長儲存,這稱為記憶體對齊。在32位編譯模式下,預設以4位元組對齊;在64位編譯模式下,預設以8位元組對齊。
為了提高存取效率,編譯器會自動進行記憶體對齊,請看下面的程式碼:
#include <stdio.h> #include <stdlib.h> struct{ int a; char b; int c; }t={ 10, 'C', 20 }; int main(){ printf("length: %d\n", sizeof(t)); printf("&a: %X\n&b: %X\n&c: %X\n", &t.a, &t.b, &t.c); system("pause"); return 0; }
在32位編譯模式下的執行結果:
length: 12
&a: B69030
&b: B69034
&c: B69038
如果不考慮記憶體對齊,結構體變數 t 所佔記憶體應該為 4+1+4 = 9 個位元組。考慮到記憶體對齊,雖然成員 b 只佔用1個位元組,但它所在的定址步長內還剩下 3 個位元組的空間,放不下一個 int 型的變量了,所以要把成員 c 放到下一個定址步長。剩下的這3個位元組,作為記憶體填充浪費掉了。請看下圖:
編譯器之所以要記憶體對齊,是為了更加高效的存取成員 c,而代價就是浪費了3個位元組的空間。
除了結構體,變數也會進行記憶體對齊,請看下面的程式碼:
#include <stdio.h>
#include <stdlib.h>
int m;
char c;
int n;
int main(){
printf("&m: %X\n&c: %X\n&n: %X\n", &m, &c, &n);
system("pause");
return 0;
}
在VS下執行:
&m: DE3384
&c: DE338C
&n: DE3388
可見它們的地址都是4的整數倍,並相互挨著。
經過筆者測試,對於全域性變數,GCC在 Debug 和 Release 模式下都會進行記憶體對齊,而VS只有在 Release 模式下才會進行對齊。而對於區域性變數,GCC和VS都不會進行對齊,不管是Debug模式還是Release模式。
改變對齊方式
記憶體對齊雖然和硬體有關,但是決定對齊方式的是編譯器,如果你的硬體是64位的,卻以32位的方式編譯,那麼還是會按照4個位元組對齊。
對齊方式可以通過編譯器引數修改,以VS2010為例,更改對齊方式的步驟為:專案 --> 屬性 --> C/C++ --> 程式碼生成 --> 結構成員對齊,如下圖所示:
最後需要說明的是:記憶體對齊不是C語言的特性,它屬於計算機的執行原理,C++、Java、Python等其他程式語言同樣也會有記憶體對齊的問題。