天天看點

四位元組記憶體對齊

首先我們先看看下面的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<strong style="BACKGROUND-COLOR: #ffff66; COLOR: black">字</strong>節對齊,我們
// 通過預編譯來通知編譯器我們以4<strong style="BACKGROUND-COLOR: #ffff66; COLOR: black">字</strong>節對齊
#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) );
	// 由于有補齊原因,是以我們需要對整個
	// 結構體進行填充,補齊對齊剩下的<strong style="BACKGROUND-COLOR: #ffff66; COLOR: black">字</strong>節
	// 以便我們可以觀察到變化
	memset( &m, 0x66, sizeof(m) );
	// 輸出結構體大小
	printf( "sizeof(MemAlign) = %d", sizeof(m) );
}
           

程式運作過程中,檢視記憶體如下:

四位元組記憶體對齊

其中,各種顔色帶下劃線的代表各個成員變量,藍色方框的代表為記憶體對齊時候填補的多餘位元組,由于這裡看不到補齊效果,我們接下來看下圖,下圖籃框包圍的位元組就是與上圖的交集以外的部分就是補齊所填充的位元組。

四位元組記憶體對齊

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

四位元組記憶體對齊