內存地址對齊,是一種在計算機內存中排列數據、訪問數據的一種方式,包含了兩種相互獨立又相互關聯的部分:基本數據對齊和結構體數據對齊。當今的計算機在計算機內存中讀寫數據時都是按字(word)大小塊來進行操作的(在32位系統中,數據總線寬度為32,每次能讀取4字節,地址總線寬度為32,因此最大的尋址空間為2^32=4GB,但是最低2位A[0],A[1]是不用於尋址,A[2-31]才能存儲器相連,因此只能訪問4的倍數地址空間,但是總的尋址空間還是2^30*字長=4GB,因此在內存中所有存放的基本類型數據的首地址的最低兩位都是0,除結構體中的成員變量)。基本類型數據對齊就是數據在內存中的偏移地址必須等於一個字的倍數,按這種存儲數據的方式,可以提升系統在讀取數據時的性能。為了對齊數據,可能必須在上一個數據結束和下一個數據開始的地方插入一些沒有用處字節,這就是結構體數據對齊。
舉個例子,假設計算機的字大小為4個字節,因此變量在內存中的首地址都是滿足4地址對齊,CPU只能對4的倍數的地址進行讀取,而每次能讀取4個字節大小的數據。假設有一個整型的數據a的首地址不是4的倍數(如下圖所示),不妨設為0X00FFFFF3,則該整型數據存儲在地址范圍為0X00FFFFF3~0X00FFFFF6的存儲空間中,而CPU每次只能對4的倍數內存地址進行讀取,因此想讀取a的數據,CPU要分別在0X00FFFFF0和0X00FFFFF4進行兩次內存讀取,而且還要對兩次讀取的數據進行處理才能得到a的數據,而一個程序的瓶頸往往不是CPU的速度,而是取決於內存的帶寬,因為CPU得處理速度要遠大於從內存中讀取數據的速度,因此減少對內存空間的訪問是提高程序性能的關鍵。從上例可以看出,采取內存地址對齊策略是提高程序性能的關鍵。
舉例:
首先我們先看看下面的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) ); }
程序運行過程中,查看內存如下:
其中,各種顏色帶下划線的代表各個成員變量,藍色方框的代表為內存對齊時候填補的多余字節,由於這里看不到補齊效果,我們接下來看下圖,下圖籃框包圍的字節就是與上圖的交集以外的部分就是補齊所填充的字節。
在最后,我在談一談關於補齊的作用,補齊其實就是為了讓這個結構體定義的數組變量時候,數組內部,也同樣滿足內存對齊的要求,為了更好的理解這點,我做了一個跟本例子相對照的圖:
參考鏈接:https://blog.csdn.net/donkeylong/article/details/4909720