計算機內存是以字節(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等其他編程語言同樣也會有內存對齊的問題。