為什麼要對齊?
效率問題,不同架構,不同處理方法。
在實際的程式開發中,為了提高資料的讀取效率,在記憶體資源足夠的情況下,一般定義資料結構時候,因該考慮四位元組對齊,其原因很簡單,現在的電腦大部分是32位機,也就是四個位元組。在cpu每次執行讀取資料時候,則相關處理資料的寄存器/ 累加器均只能處理32位元據,則只能讀取(一次)32位元據。有些情況下位元組對齊的資料結構,要比非對齊的資料結構上佔用更少空間。
對齊跟資料在記憶體中的位置有關。如果一個變數的記憶體位址正好位於它長度的整數倍,他就被稱做自然對齊。比如在32位cpu下,假設一個整型變數的地址為0x00000004,那它就是自然對齊的。
需要位元組對齊的根本原因在於CPU訪問資料的效率問題。假設上面整型變數的地址不是自然對齊,比如為0x00000002,則CPU如果取它的值的話需要訪問兩次記憶體,第一次取從0x00000002-0x00000003的一個short,第二次取從0x00000004-0x00000005的一個short然後組合得到所要的資料,如果變數在0x00000003地址上的話則要訪問三次記憶體,第一次為char,第二次為short,第三次為char,然後組合得到整型資料。而如果變數在自然對齊位置上,則只要一次就可以取出資料。
在C語言中,結構是一種複合資料型別,其構成元素既可以是基礎資料型別 (Elementary Data Type)(如int、long、float等)的變數,也可以是一些複合資料型別(如數組、結構、聯合等)的資料單元。在結構中,編譯器為結構的每個成員按其常態範圍(alignment)分配空間。各個成員按照它們被聲明的順序在記憶體中順序儲存,第一個成員的地址和整個結構的地址相同。為了使CPU能夠對變數進行快速的訪問,變數的起始地址應該具有某些特性,即所謂的”對齊”.
比如4位元組的int型,其起始地址應該位於4位元組的邊界上,即起始地址能夠被4整除.
記憶體對齊的三條重要原則
1:資料成員對齊規則:結構(struct)(或聯合(union))的資料成員,第一個資料成員放在offset為0的地方,以後每個資料成員儲存的起始位置要從該成員大小的整數倍開始(比如int在32位機為4位元組,則要從4的整數倍地址開始儲存。
2:結構體作為成員:如果一個結構裡有某些結構體成員,則結構體成員要從其內部最大元素大小的整數倍地址開始儲存.(struct a裡存有struct b,b裡有char,int
,double等元素,那b應該從8的整數倍開始儲存.)
3:收尾工作:結構體的總大小,也就是sizeof的結果,.必須是其內部最大成員的整數倍.不足的要補齊.
example1:
[cpp]
view plaincopyprint?
- typedef struct bb
- {
- int id; //[0]....[3]
- double weight; //[7].....[15] 原則1
- float height; //[16]..[19],總長要為8的整數倍,補齊[20]...[23] 原則3
- }BB;
- typedef struct aa
- {
- char name[2]; //[0],[1]
- int id; //[4]...[7] 原則1
- double score; //[8]....[15]
- short grade; //[16],[17]
- BB b; //[24]......[47] 原則2
- }AA;
typedef struct bb{ int id; //[0]....[3] double weight; //[7].....[15] 原則1 float height; //[16]..[19],總長要為8的整數倍,補齊[20]...[23] 原則3}BB;typedef struct aa{ char name[2]; //[0],[1] int id; //[4]...[7] 原則1 double score; //[8]....[15] short grade; //[16],[17] BB b; //[24]......[47] 原則2}AA;
cout<<sizeof(a)<<" "<<sizeof(b)<<endl;
輸出:48 24
在代碼前加一句#pragma pack(1),上面的代碼輸出為
32 16
bb是4+8+4=16,aa是2+4+8+2+16=32;
這是理想中的沒有記憶體對齊,#pragma pack(1),告訴編譯器,所有的對齊都按照1的整數倍對齊,換句話說就是沒有對齊規則.
example2:
對於標準資料類型,它的地址只要是它的長度的整數倍就行了,而非標準資料類型按下面的原則對齊:
數組 :按照基礎資料型別 (Elementary Data Type)對齊,第一個對齊了後面的自然也就對齊了。
聯合 :按其包含的長度最大的資料類型對齊。
結構體: 結構體中每個資料類型都要對齊。
比如有如下一個結構體:
[cpp]
view plaincopyprint?
- struct stu
- {
- char sex;
- int length;
- char name[10];
- };
struct stu{ char sex; int length; char name[10];};
struct stu my_stu;
cout << sizeof(my_stu) << endl;
輸出 :20
由於在x86下,GCC預設按4位元組對齊,它會在sex後面跟name後面分別填充三個和兩個位元組使length和整個結構體對齊。於是我們sizeof(my_stu)會得到長度為20,而不是15.
char 佔1個位元組,填充3個位元組,int佔4個位元組,char數組佔10個位元組,一共18個位元組,18不是4的整數倍,結尾填充2個位元組,一共20個位元組。
example3:
[cpp]
view plaincopyprint?
- struct MyStruct
- {
- double doub;
- char ch;
- int i;
- };
struct MyStruct { double doub; char ch; int i; };
對結構MyStruct採用sizeof會出現什麼結果呢?sizeof(MyStruct)為多少呢?
sizeof(ms)為16。這是編譯器對變數儲存的一個特殊處理。為了提高CPU的儲存速度,編譯器對一些變數的起始地址做了“對齊”處理。在預設情況下,編譯器規定各成員變數存放的起始地址相對於結構的起始地址的位移量必須為該變數的類型所佔用的位元組數的倍數。
為上面的結構分配空間的時候,編譯器根據成員變數出現的順序和對齊,先為第一個成員doub分配空間,其起始地址跟結構的起始地址相同(剛好位移量0剛好為sizeof(double)的倍數),該成員變數佔用sizeof(double)=8個位元組;接下來為第二個成員ch分配空間,這時下一個可以分配的地址對於結構的起始地址的位移量為8,是sizeof(char)的倍數,所以把ch存放在位移量為8的地方滿足對齊,該成員變數佔用sizeof(char)=1個位元組;接下來為第三個成員i分配空間,這時下一個可以分配的地址對於結構的起始地址的位移量為9,不是sizeof(int)=4的倍數,為了滿足對齊對位移量的約束問題,編譯器自動填滿3個位元組(這三個位元組沒有放什麼東西),這時下一個可以分配的地址對於結構的起始地址的位移量為12,剛好是sizeof(int)=4的倍數,所以把i存放在位移量為12的地方,該成員變數佔用sizeof(int)=4個位元組;這時整個結構的成員變數已經都分配了空間,總的佔用的空間大小為:8+1+3+4=16,剛好為結構的位元組邊界數(即結構中佔用最大空間的類型所佔用的位元組數sizeof(double)=8)的倍數,所以沒有空缺的位元組需要填充。所以整個結構的大小為:sizeof(MyStruct)=8+1+3+4=16,其中有3個位元組是編譯器自動填滿的,沒有放任何有意義的東西。
各成員變數在存放的時候根據在結構中出現的順序依次申請空間,同時按照上面的對齊調整位置,空缺的位元組編譯器會自動填滿。同時編譯器為了確保結構的大小為結構的位元組邊界數(即該結構中佔用最大空間的類型所佔用的位元組數)的倍數,所以在為最後一個成員變數申請空間後,還會根據需要自動填滿空缺的位元組。
下面再舉個例子,交換一下上面的MyStruct的成員變數的位置,使它變成下面的情況:
[cpp]
view plaincopyprint?
- struct MyStruct
- {
- char ch;
- double doub;
- int i;
- };
struct MyStruct { char ch; double doub; int i; };
在編譯器環境下,可以得到sizeof(MyStruct)為24。結合上面提到的分配空間的一些原則,分析下編譯器怎麼樣為上面 的結構分配空間:
struct MyStruct
{
char ch; // 位移量為0,滿足對齊,ch佔用1個位元組;
double doub; //下一個可用的地址的位移量為1,不是sizeof(double)=8
//的倍數,需要補足7個位元組才能使位移量變為8(滿足對齊
//方式),因此編譯器自動填滿7個位元組,doub存放在位移量為8
//的地址上,它佔用8個位元組。
int i; //下一個可用的地址的位移量為16,是sizeof(int)=4的倍
//數,滿足int的對齊,所以不需要編譯器自動填滿,i存
//放在位移量為16的地址上,它佔用4個位元組。
}; //所有成員變數都分配了空間,空間總的大小為1+7+8+4=20,不是結構
//的節邊界數(即結構中佔用最大空間的類型所佔用的位元組數sizeof
//(double)=8)的倍數,所以需要填充4個位元組,以滿足結構的大小為
//sizeof(double)=8的倍數。
所以該結構總的大小為:sizeof(MyStruct)為1+7+8+4+4=24。其中總的有7+4=11個位元組是編譯器自動填滿的,沒有放任何有意義的東西。
example4:
[cpp]
view plaincopyprint?
- struct{
- short a1;
- short a2;
- short a3;
- }A;
- struct{
- long a1;
- short a2;
- }B;
struct{ short a1; short a2; short a3;}A;struct{ long a1; short a2;}B;
sizeof(A)=6, sizeof(B)=8
成員對齊有一個重要的條件,即每個成員按自己的方式對齊.其對齊的規則是,每個成員按其類型的對齊參數(通常是這個類型的大小)和指定對齊參數(這裡預設是8位元組)中較小的一個對齊.並且結構的長度必須為所用過的所有對齊參數的整數倍,不夠就補空位元組.結構體A中有3個short類型變數,各自以2位元組對齊,結構體對齊參數按預設的8位元組對齊,則a1,a2,a3都取2位元組對齊,則sizeof(A)為6,其也是2的整數倍;B中a1為4位元組對齊,a2為2位元組對齊,結構體預設對齊參數為8,則a1取4位元組對齊,a2取2位元組對齊,結構體大小6位元組,6不為4的整數倍,補空位元組,增到8時,符合所有條件,則sizeof(B)為8
example5:
[cpp]
view plaincopyprint?
- typedef struct ms1
- {
- char a;
- short b;
- double c;
- } MS1;
- typedef struct ms2
- {
- char a;
- MS1 b;
- } MS2;
typedef struct ms1{ char a; short b; double c;} MS1;typedef struct ms2{ char a; MS1 b;} MS2;
cout<<sizeof(MS1)<<" "<<sizeof(MS2)<<endl;
輸出:16 24
ANSI C保證結構體中各欄位在記憶體中出現的位置是隨它們的聲明順序依次遞增的,並且第一個欄位的首地址等於整個結構體執行個體的首地址。
許多實際的電腦系統對基本類型資料在記憶體中存放的位置有限制,它們會要求這些資料的首地址的值是某個數k(通常它為4或8)的倍數,這就是所謂的記憶體對齊,而這個k則被稱為該資料類型的對齊模數(alignment modulus)。當一種類型S的對齊模數與另一種類型T的對齊模數的比值是大於1的整數,我們就稱類型S的對齊要求比T強(嚴格),而稱T比S弱(寬鬆)。這種強制的要求一來簡化了處理器與記憶體之間傳輸系統的設計,二來可以提升讀取資料的速度。比如這麼一種處理器,它每次讀寫記憶體的時候都從某個8倍數的地址開始,一次讀出或寫入8個位元組的資料,假如軟體能保證double類型的資料都從8倍數地址開始,那麼讀或寫一個double類型資料就只需要一次記憶體操作。否則,我們就可能需要兩次記憶體操作才能完成這個動作,因為資料或許恰好橫跨在兩個符合對齊要求的8位元組記憶體塊上。某些處理器在資料不滿足對齊要求的情況下可能會出錯,但是Intel的IA32架構的處理器則不管資料是否對齊都能正確工作。不過Intel奉勸大家,如果想提升效能,那麼所有的程式資料都應該儘可能地對齊。Win32平台下的微軟C
編譯器(cl.exe for 80x86)在預設情況下採用如下的對齊規則: 任何基礎資料型別 (Elementary Data Type)T的對齊模數就是T的大小,即sizeof(T)。比如對於double類型(8位元組),就要求該類型資料的地址總是8的倍數,而 char類型資料(1位元組)則可以從任何一個地址開始。ANSI C規定一種結構類型的大小是它所有欄位的大小以及欄位之間或欄位尾部的填充區大小之和。嗯?填充區?對,這就是為了使結構體欄位滿足記憶體對齊要求而額外分配給結構體的空間。
MS1:Bytes: 1 1 2 4 8
sizeof(short)等於2,b欄位應從偶數地址開始,所以a的後面填充一個位元組,而sizeof(double)等於8,c欄位要從8倍數地址開始,前面的a、b欄位加上填充位元組已經有4
bytes,所以b後面再填充4個位元組就可以保證c欄位的對齊要求了。sizeof(MS1)等於16,b的位移是2,c的位移是8。
MS3中記憶體要求最嚴格的欄位是c,那麼MS3類型資料的對齊模數就與double的一致(為8),a欄位後面應填充7個位元組,因此MS4的布局應該是:
Bytes: 1 7 16
顯然,sizeof(MS4)等於24,b的位移等於8。