內存對齊

首先我們先看看下面的C語言的結構體:

typedef struct MemAlign
{
	int a;
	char b[3];
	int c;
}MemAlign;

    以上這個結構體佔用內存多少空間呢?也許你會說,這個簡單,計算每個類型的大小,將它們相加就行了,以32爲平臺爲例,int類型佔4字節,char佔用1字節,所以:4 + 3 + 4 = 11,那麼這個結構體一共佔用11字節空間。好吧,那麼我們就用實踐來證明是否正確,我們用sizeof運算符來求出這個結構體佔用內存空間大小,sizeof(MemAlign),出乎意料的是,結果居然爲12?看來我們錯了?當然不是,而是這個結構體被優化了,這個優化有個另外一個名字叫“對齊”,那麼這個對齊到底做了什麼樣的優化呢,聽我慢慢解釋,再解釋之前我們先看一個圖,圖如下:

    相信學過彙編的朋友都很熟悉這張圖,這張圖就是CPU與內存如何進行數據交換的模型,其中,左邊藍色的方框是CPU,右邊綠色的方框是內存,內存上面的0~3是內存地址。這裏我們這張圖是以32位CPU作爲代表,我們都知道,32位CPU是以雙字(DWORD)爲單位進行數據傳輸的,也正因爲這點,造成了另外一個問題,那麼這個問題是什麼呢?這個問題就是,既然32位CPU以雙字進行數據傳輸,那麼,如果我們的數據只有8位或16位數據的時候,是不是CPU就按照我們數據的位數來進行數據傳輸呢?其答案是否定的,如果這樣會使得CPU硬件變的更復雜,所以32位CPU傳輸數據無論是8位或16位都是以雙字進行數據傳輸。那麼也罷,8位或16位一樣可以傳輸,但是,事情並非像我們想象的那麼簡單,比如,一個int類型4字節的數據如果放在上圖內存地址1開始的位置,那麼這個數據佔用的內存地址爲1~4,那麼這個數據就被分爲了2個部分,一個部分在地址0~3中,另外一部分在地址4~7中,又由於32位CPU以雙字進行傳輸,所以,CPU會分2次進行讀取,一次先讀取地址0~3中內容,再一次讀取地址4~7中數據,最後CPU提取並組合出正確的int類型數據,捨棄掉無關數據。那麼反過來,如果我們把這個int類型4字節的數據放在上圖從地址0開始的位置會怎樣呢?讀到這裏,也許你明白了,CPU只要進行一次讀取就可以得到這個int類型數據了。沒錯,就是這樣,這次CPU只用了一個週期就得到了數據,由此可見,對內存數據的擺放是多麼重要啊,擺放正確位置可以減少CPU的使用資源。

那麼,內存對齊有哪些原則呢?我總結了一下大致分爲三條:
第一條:第一個成員的首地址爲0
第二條:每個成員的首地址是自身大小的整數倍
       第二條補充:以4字節對齊爲例,如果自身大小大於4字節,都以4字節整數倍爲基準對齊。
第三條:最後以結構總體對齊。
        第三條補充:以4字節對齊爲例,取結構體中最大成員類型倍數,如果超過4字節,都以4字節整數倍爲基準對齊。(其中這一條還有個名字叫:“補齊”,補齊的目的就是多個結構變量挨着擺放的時候也滿足對齊的要求。)

    上述的三原則聽起來還是比較抽象,那麼接下來我們通過一個例子來加深對內存對齊概念的理解,下面是一個結構體,我們動手算出下面結構體一共佔用多少內存?假設我們以32位平臺並且以4字節對齊方式:

#pragma pack(4)
typedef struct MemAlign
{
	char a[18];
	double b;	
	char c;
	int d;	
	short e;	
}MemAlign;
下圖爲對齊後結構如下:

我們就以這個圖來講解是如何對齊的:
第一個成員(char a[18]):首先,假設我們把它放到內存開始地址爲0的位置,由於第一個成員佔18個字節,所以第一個成員佔用內存地址範圍爲0~18。
第二個成員(double b):由於double類型佔8字節,又因爲8字節大於4字節,所以就以4字節對齊爲基準。由於第一個成員結束地址爲18,那麼地址18並不是4的整數倍,我們需要再加2個字節,也就是從地址20開始擺放第二個成員。
第三個成員(char c):由於char類型佔1字節,任意地址是1字節的整數倍,所以我們就直接將其擺放到緊接第二個成員之後即可。
第四個成員(int d):由於int類型佔4字節,但是地址29並不是4的整數倍,所以我們需要再加3個字節,也就是從地址32開始擺放這個成員。
第五個成員(short e):由於short類型佔2字節,地址36正好是2的整數倍,這樣我們就可以直接擺放,無需填充字節,緊跟其後即可。
    這樣我們內存對齊就完成了。但是離成功還差那麼一步,那是什麼呢?對,是對整個結構體補齊,接下來我們就補齊整個結構體。那麼,先讓我們回顧一下補齊的原則:“以4字節對齊爲例,取結構體中最大成員類型倍數,如果超過4字節,都以4字節整數倍爲基準對齊。”在這個結構體中最大類型爲double類型(佔8字節),又由於8字節大於4字 節,所以我們還是以4字節補齊爲基準,整個結構體結束地址爲38,而地址38並不是4的整數倍,所以我們還需要加額外2個字節來填充結構體,如下圖紅色的就是補齊出來的空間:

到此爲止,我們內存對齊與補齊就完畢了!接下來我們用實驗來證明真理,程序如下:

#include <stdio.h>
#include <memory.h>

// 由於VS2010默認是8字節對齊,我們
// 通過預編譯來通知編譯器我們以4字節對齊
#pragma pack(4)

// 用於測試的結構體
typedef struct MemAlign
{
char a[18]; // 18 bytes
double b; // 08 bytes
char c; // 01 bytes
int d; // 04 bytes
short e; // 02 bytes
}MemAlign;

int main()
{
// 定義一個結構體變量
MemAlign m;
// 定義個以指向結構體指針
MemAlign *p = &m;
// 依次對各個成員進行填充,這樣我們可以
// 動態觀察內存變化情況
memset( &m.a, 0x11, sizeof(m.a) );
memset( &m.b, 0x22, sizeof(m.b) );
memset( &m.c, 0x33, sizeof(m.c) );
memset( &m.d, 0x44, sizeof(m.d) );
memset( &m.e, 0x55, sizeof(m.e) );
// 由於有補齊原因,所以我們需要對整個
// 結構體進行填充,補齊對齊剩下的字節
// 以便我們可以觀察到變化
memset( &m, 0x66, sizeof(m) );
// 輸出結構體大小
printf( “sizeof(MemAlign) = %d”, sizeof(m) );
}

程序運行過程中,查看內存如下:

其中,各種顏色帶下劃線的代表各個成員變量,藍色方框的代表爲內存對齊時候填補的多餘字節,由於這裏看不到補齊效果,我們接下來看下圖,下圖籃框包圍的字節就是與上圖的交集以外的部分就是補齊所填充的字節。

在最後,我在談一談關於補齊的作用,補齊其實就是爲了讓這個結構體定義的數組變量時候,數組內部,也同樣滿足內存對齊的要求,爲了更好的理解這點,我做了一個跟本例子相對照的圖:


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