內存對齊分配策略(含位域模式)

1. 內存對齊定義

如今使用的計算機中內存空間都是按照字節劃分的,從理論上講彷佛對任何類型的變量的訪問能夠從任何地址開始,可是實際上計算機系統對於基本數據類型在內存中的存放位置都有限制,要求這些數據存儲首地址是某個數K的倍數,這樣各類基本數據類型在內存衝就是按照必定的規則排列的,而不是一個緊挨着一個排放,這就是內存對齊.html

對齊模數  內存對齊中指定的對齊數值K成爲對齊模數(Alignment Modulus).當一種類型S的對齊模數與另外一種類型T的對齊模數的比值是大於1的整數,咱們就稱類型S的對齊要求比T強(嚴格),而稱T比S弱(寬鬆).linux

2. 內存對齊的好處

內存對齊做爲一種強制的要求,第一簡化了處理器與內存之間傳輸系統的設計,第二能夠提高讀取數據的速度.各個硬件平臺對存儲空間的處理上有很大的不一樣.一些平臺對某些特定類型的數據只能從某些特定地址開始存取.好比有些架構的CPU在訪問一個沒有進行對齊的變量的時候會發生錯誤,那麼在這種架構下編程必須保證字節對齊.其餘平臺可能沒有這種狀況,可是最多見的是若是不按照適合其平臺要求對數據存放進行對齊,會在存取效率上帶來損失.好比有些平臺每次讀都是從偶地址開始,若是一個int型(假設爲32位系統)若是存放在偶地址開始的地方,那麼一個讀週期就能夠讀出這32bit,而若是存放在奇地址開始的地方,就須要2個讀週期,並對兩次讀出的結果的高低字節進行拼湊才能獲得該32bit數據.顯然在讀取效率上降低不少. 編程

Intel的IA32架構的處理器則無論數據是否對齊都能正確工做.可是若是想提高性能,應該注意內存對齊方式. 
ANSI C標準中並無規定相鄰聲明的變量在內存中必定要相鄰.爲了程序的高效性,內存對齊問題由編譯器自行靈活處理,這樣致使相鄰的變量之間可能會有一些填充字節.對於基本數據類型(int char等),他們佔用的內存空間在一個肯定硬件系統下有肯定的值.ANSI C規定一種結構類型的大小是它全部字段的大小以及字段之間或字段尾部的填充區大小之和.windows

3. 內存對齊策略

微軟C編譯器(cl.exe for 80×86)的對齊策略:
第一: 結構體變量的首地址可以被其最寬基本類型成員的大小所整除; 
備註: 編譯器在給結構體開闢空間時,首先找到結構體中最寬的基本數據類型,而後尋找內存地址能被該基本數據類型所整除的位置,做爲結構體的首地址.將這個最寬的基本數據類型的大小做爲上面介紹的對齊模數. 
第二: 結構體每一個成員相對於結構體首地址的偏移量(offset)都是成員大小的整數倍,若有須要編譯器會在成員之間加上填充字節(internal adding); 
備註: 爲結構體的一個成員開闢空間以前,編譯器首先檢查預開闢空間的首地址相對於結構體首地址的偏移是不是本成員的整數倍,如果,則存放本成員,反之,則在本成員和上一個成員之間填充必定的字節,以達到整數倍的要求,也就是將預開闢空間的首地址後移幾個字節. 
第三: 結構體的總大小爲結構體最寬基本類型成員大小的整數倍,若有須要,編譯器會在最末一個成員以後加上填充字節(trailing padding). 
備註: 結構體總大小是包括填充字節,最後一個成員知足上面兩條之外,還必須知足第三條,不然就必須在最後填充幾個字節以達到本條要求.數組

填充字節就是爲了使結構體字段知足內存對齊要求而額外分配給結構體的空間.對於結構體自己也存在着對齊要求,ANSI C標準規定結構體類型的對齊要求不能比它全部字段中要求最嚴格的那個寬鬆,可是能夠更嚴格(但此非強制要求,VC7.1就僅僅是讓它們同樣嚴格).C標準保證,任何類型(包括自定義結構類型)的數組所佔空間的大小必定等於一個單獨的該類型數據的大小乘以數組元素的個數.換句話說,數組各元素之間不會有空隙.架構

總結規則以下: 
0: 結構體變量的首地址可以被其最寬基本類型成員的大小所整除 
1: VC6和VC71默認的內存對齊方式是 #pragam pack(8) 
2: 結構體中每一個成員按其類型的對齊參數(一般是這個類型的大小)和指定對齊參數中較小的一個對齊. 
3: 結構體每一個成員相對於結構體首地址的偏移量都是成員大小的整數倍. 
4: 結構體自己也存在着對齊要求規則,不能比它全部字段中要求最嚴格的那個寬鬆. 
5: 結構體的總大小爲結構體最寬基本類型成員大小的整數倍,且應儘可能節省內存. 
6: 在GCC中,對齊模數的準則是:對齊模數最大隻能是 4,也就是說,即便結構體中有double類型,對齊模數仍是4,因此對齊模數只能是1,2,4. 
    並且在上述的規則中,第3條裏,offset必須是成員大小的整數倍: 
    (1): 若是這個成員大小小於等於4則按照上述準則是可行的, 
    (2): 若是成員的大小大於4,則結構體每一個成員相對於結構體首地址的偏移量只能按照是4的整數倍來進行判斷是否添加填充.佈局

例子1(VC8):性能

typedef struct ms1 { 
  char a; 
  int b; 
} MS1;

typedef struct ms2 { 
  int a; 
  char b; 
} MS2; 

MS1中有最強對齊要求的是b字段(int類型),字段a相對於首地址偏移量爲0(1的倍數),直接存放,此時若是直接存放字段b,則字段b相對於結構體變量首地址的偏移量爲1(不是4的倍數),填充3字節,b由偏移地址爲4開始存放.也就是遵循了第2條與第3條規則,而對於結構體變量自己,根據規則4,對齊參數至少應該爲4.根據規則5,sizeof(MS1) = 8; 一樣MS2分析獲得的結果也是如此.spa

例子2VC8:設計

typedef struct ms3 { 
  char a; 
  short b; 
  double c; 
} MS3;

typedef struct ms4 { 
  char a; 
  MS3 b; 
} MS4; 

MS3中內存要求最嚴格的字段是c(8字節),MS3的對齊參數也是8字節; 那麼MS4類型數據的對齊模數就與MS3中的double一致(爲8),a字段後面應填充7個字節.sizeof(MS3) = 16; sizeof(MS4) = 24; 

注意規則5中是說,結構體的總大小爲結構體最寬基本類型成員大小的整數倍.注意是基本類型,這裏的MS3不是基本類型. 
對齊模數的選擇只能是根據基本數據類型,因此對於結構體中嵌套結構體,只能考慮其拆分的基本數據類型.

例子3(GCC): 

struct T { 
  char ch; 
  double d ; 
}; 

在GCC下,sizeof(T)應該等於12個字節.VC8下爲16字節. 

ch爲1字節,沒有問題的,以後d的大小大於4,對於d的對齊模數只能是4,相對於結構體變量的首地址偏移量也只能是4,而不能使8的整數倍,由偏移量4開始存放,結構體共佔12字節. 
這裏並無執行第5條規則.

位域狀況: 
C99規定int、unsigned   int和bool能夠做爲位域類型.但編譯器幾乎都對此做了擴展,容許其它類型類型的存在. 
若是結構體中含有位域(bit-field),總結規則以下 
1) 若是相鄰位域字段的類型相同,且其位寬之和小於類型的sizeof大小,則後面的字段將緊鄰前一個字段存儲,直到不能容納爲止; 
2) 若是相鄰位域字段的類型相同,但其位寬之和大於類型的sizeof大小,則後面的字段將重新的存儲單元開始,其偏移量爲其類型大小的整數倍; 
3) 若是相鄰的位域字段的類型不一樣,則各編譯器的具體實現有差別,VC6採起不壓縮方式(不一樣位域字段存放在不一樣的位域類型字節中),Dev-C++和GCC都採起壓縮方式; 
4)若是位域字段之間穿插着非位域字段,則不進行壓縮; 
5) 結構體的總大小爲結構體最寬基本類型成員大小的整數倍,且應儘可能節省內存. 
備註:當兩字段類型不同的時候,對於不壓縮方式,例如:

struct N { 
  char c:2; 
  int i:4; 
}; 

依然要知足不含位域結構體內存對齊準則第3條,i成員相對於結構體首地址的偏移應該是4的整數倍,因此c成員後要填充3個字節,而後再開闢4個字節的空間做爲int型,其中4位用來存放i,因此上面結構體在VC中所佔空間爲8個字節; 

而對於採用壓縮方式的編譯器來講,遵循不含位域結構體內存對齊準則第3條,不一樣的是,若是填充的3個字節能容納後面成員的位,則壓縮到填充字節中,不能容納,則要單獨開闢空間,因此上面結構體N在GCC或者Dev- C++中所佔空間應該是4個字節.

例子4: 

typedef struct { 
  char c:2; 
  double i; 
  int c2:4; 
}N3;

按照含位域規則4,在GCC下佔據的空間爲16字節,在VC下佔據的空間是24個字節.

結論: 
-------- 
定義結構體的時候,成員最好能從大到小來定義,那樣能相對的省空間.例如以下定義: 

struct A { 
  double d; 
  int i; 
  char c; 
}; 

那麼,不管是windows下的vc系列編譯器,仍是linux下的gcc,都是16字節.

例子5: 

typedef union student{ 
  char name[10]; 
  long sno; 
  char sex; 
  float score [4]; 
} STU; 
STU aa[5]; 
cout<<sizeof(aa)<<endl; 

union是可變的以其成員中最大的成員做爲該union的大小16*5=80

例子6: 

typedef struct student{ 
  char name[10]; 
  long sno; 
  char sex; 
  float score [4]; 
} STU; 
STU aa[5]; 
cout<<sizeof(aa)<<endl; 

STU佔空間爲:10字節(char)+空2字節+4字節(long)+1字節(char)+空3字節+16字節(float)=36字節,36*5=180字節

例子7(VC8.0):

typedef struct bitstruct { 
  int b1:5; 
  int b2:2; 
  int b3:3; 
}bitstruct;

int _tmain(int argc, _TCHAR* argv[]) { 
  bitstruct b; 
  memcpy(&b,"EM",sizeof(b)); 
  cout<<sizeof(b)<<endl; 
  cout<<b.b1<<endl<<b.b2<<endl<<b.b3; 
  return 0; 
} 

對於bitstruct是含有位域的結構體,sizeof(int)爲4字節,按照規則一、2,首先b1佔起始的5個bit, 根據含位域規則1, b2緊跟存放,b3也是緊跟存放的. 

根據規則5,獲得sizeof(bitstruct) = 4. 
如今主流的CPU,intel系列的是採用的little endian的格式存放數據,motorola系列的CPU採用的是big endian. 
以主流的little endian分析: 
在進行內存分配的時候,首先分配bitstruct的第一個成員類型int(4字節),這四個字節的存放按照低字節存儲在低地址中的原則. 
int共4個字節: 
第4個字節 - 第3個字節 - 第2個字節 - 第1個字節,

在內存中的存放方式以下所示.

然後爲b1分配5位,這裏優先分配的應該是低5位,也就是第一個字節的低5位. 
繼而分配b2的2個字節,也就是第1個字節中緊接着的2位. 
最後分配b3的3位,按照規則一、2,b3仍是緊接着存放的,b3的最低位是第一個字節的最高位,高兩位爲第2個字節的低兩位. 
內存分配圖以下所示:


字符E二進制爲0100 0101,字符M的二進制爲0100 1101,在內存中存放以下所示:


memcpy爲按位拷貝的,因此兩片內存區能夠直接對應上,獲得 
b1的二進制形式爲:00101 ,高位爲0,正數,5 
b2的二進制形式爲:10 ,高位爲1,負數,取反加1,添加符號,-2 
b3的二進制形式爲:b3的最低一位是0,高位爲01,拼接後爲010,正數,2

內存分配狀況感受蠻奇怪的,按以下修改例7,b1應該爲5,b2爲-2,b3爲-6,VC8.0下驗證正確. 

typedef struct bitstruct { 
  int b1:5; 
  int b2:2; 
  int b3:4; 
}bitstruct;

int _tmain(int argc, _TCHAR* argv[]) { 
  bitstruct b; 
  memcpy(&b,"EM",sizeof(b)); 
  cout<<sizeof(b)<<endl; 
  cout<<b.b1<<endl<<b.b2<<endl<<b.b3; 
  return 0; 
}

4. 定義數組時的內存佈局及內存字節對齊

int b=10;

int a[3]={1,2,3};

int c=11;



int b=0x01020304;

char ch='a';

對於一個數0x01020304

使用Little Endian方式時,低地址存放低字節,由低地址向高地址存放爲:4->3->2->1 
而使用Big Endian方式時, 低地址存放高字節,由低地址向高地址存放爲:1->2->3->4

而在Little Endian模式中,b的地址所指的就是:低地址(存放的是最低的字節)

本文轉自:http://www.cnblogs.com/alex-tech/archive/2011/03/24/1993856.html

相關文章
相關標籤/搜索