內存對齊之深度探索

編譯器爲何要替咱們內存對齊?

 
學了計算機組成原理,瞭解了內存的基本單元是一個字節,內存能夠隨機尋址,因而乎我天真的認爲內存就是一個字節型的容器,基本單位是單個字節。
 
Figure 1. 我眼中的內存空間佈局
 
How Programmers See Memory
 
悲劇的是,內存讀寫的真正訪問者cpu不是這麼想的。cpu是根據內存訪問粒度(memory access granularity,下文簡寫成MAG)來讀取內存,MAG就是cpu一次內存訪問操做的數據量,具體數值依賴於特定的平臺,通常是2byte、4byte、8byte。
 
Figure 2. cpu眼中的內存空間佈局
 
How Some Processors See Memory
 
所以,程序員和cpu看待內存空間佈局是存在差別的。唉,既不能過多的苛刻程序員,又要讓cpu舒服,怎麼辦呢?只好讓編譯器來對咱們的代碼進行隱式的內存對齊(固然它能作的只是幫程序中的數據進行內存對齊,至於直接用指針去訪問內存,它是不會管的)。 向開發編譯器的大大們致敬!
 

內存對齊的好處

 
 
下面用一個小實例來剖析內存對齊的好處:很簡單,在32位的機器下,分別訪問一個內存對齊的地址空間(從地址0開始)和一個沒有對齊的地址空間(從地址1開始),讀取四個字節到cpu的寄存器中,比較二者的讀取過程。
 
Case1:內存訪問粒度爲1個字節(cpu眼中的內存模型等價於程序員眼中的內存模型):
 
Figure 3. MAG=1
 
Single-byte memory access granularity
 
Result:讀取4個字節,二者都須要進行4次內存訪問操做。打平,在MAG=1的狀況下不須要考慮內存對齊。
 
Case2:內存訪問粒度爲2個字節:
 
Figure 4. MAG=2
 
Double-byte memory access granularity
 
Result:讀取4個字節,左邊的(內存對齊地址)只須要進行2次內存訪問操做,右邊的須要進行3次內存訪問操做+附加操做(見下文)。內存對齊地址取勝!
 
Case3:內存訪問粒度爲4個字節:
 
Figure 5. MAG=4
 
Quad-byte memory access granularity
 
Result:讀取4個字節,左邊的只須要進行1次內存訪問操做,右邊的須要進行2次內存訪問操做+附加操做。內存對齊地址再次取勝!
 
Conclusion:
內存對齊地址vs沒有內存對齊的地址,在三種不一樣的內存訪問粒度下,取得了2勝一平的完勝戰績。對於32位的機器,實際的內存訪問粒度是4個字節,緣由以下:
  • 每一次內存訪問操做須要一個常量開銷;
  • 在數據量必定的狀況下,減小內存訪問操做,能提升程序運行性能;
  • 增大內存訪問粒度(固然不超過數據總線的帶寬),能減小內存訪問操做(從上面的實例就可以看出來);
一句話,內存對齊確實能夠提升程序性能。
 

cpu如何處理沒有內存對齊的數據訪問?

 
繼續分析上面那個實例,在內存訪問粒度爲二、從地址1開始讀取四個字節的cpu處理過程(硬件方式):
  1. 讀取數據所在的第一塊內存空間(0-1),移除多餘字節(0);
  2. 讀取數據所在的第二塊內存空間(2-3);
  3. 讀取數據所在的第三塊內存空間(4-5),移除多餘字節(5);
  4. 把三塊數據拼接起來(1-4),放入寄存器中。
訪問一塊相同大小的數據,內存對齊的優點是多麼的巨大!
若是cpu能這麼來處理,也只不過是影響了咱們程序的運行性能,至少仍是能運行的!悲劇的是,之前的cpu並無這麼「勤快」,遇到沒有內存對齊的數據訪問,它會直接拋出一個異常:操做系統可能會響應這個異常,用軟件的方式來處理,性能只會更差;或者程序直接崩潰掉。 一句話,內存對齊的代碼確實具備更高的可移植性!
 
 
 
 
結構體在內存中的對齊方式
我的總結:
結構體默認的對齊模數不是sizeof(結構體),而是結構體成員中最大的對齊模數。例如
struct A{
    int a;//默認對齊模數k(int)=sizeof(int)=4
    char  b;//默認對齊模數k(char)=sizeof(char)=1
    struct B d;//默認對齊模數k(struct B)爲B成員最大的對齊模數即k(double)=8

};//A默認對齊模數k(struct A)爲A成員最大的對齊模數k(struct B)=8

struct B{
    int a;//默認對齊模數k(int)=sizeof(int)=4
    char  b;//默認對齊模數k(char)=sizeof(char)=1
    double c;//默認對齊模數k(double)=sizeof(double)=8
    short//默認對齊模數k(short)=sizeof(short)=2
};//B默認對齊模數k(struct B)爲B成員最大的對齊模數即k(double)=8

由上面的兩個結構體能夠看出 :c++

基本數據類型的默認對齊模數k=sizeof(type);程序員

複雜數據類型如結構體,其默認對齊模數k=max{各個成員的默認對齊模數};編程

 

存儲規則:數組

1.當系統指定了對齊模數k時,不一樣類型的數據按照k(m)=min{默認的對齊模數k,系統指定的k}模數來存儲數據;數據結構

2.該類型的數據起始地址add%k(m)=0;爲何要這樣知足這個條件呢?架構

其緣由是各個硬件平臺對存儲空間的處理上有很大的不一樣。一些平臺對某些特定類型的數據只能從某些特定地址開始存取。好比有些平臺每次讀都是從偶地址開始,若是一個int型(假設爲32位系統)若是存放在偶地址開始的地方,那 麼一個讀週期就能夠讀出這32bit,而若是存放在奇地址開始的地方,就須要2個讀週期,並對兩次讀出的結果的高低字節進行拼湊才能獲得該32bit數 據。顯然在讀取效率上降低不少。函數

3.該類型的數據大小size%k(m)=0,不足的要補齊(結構體中常常須要補齊);爲何要這樣知足這個條件呢?佈局

由於考慮到該類型的數組,按照數組的定義,數組中全部元素都是緊挨着的。性能

 

 

 

 

結構體內存分配問題(轉)

1)sizeof也能夠對一個函數調用求值,其結果是函數返回類型的大小,函數並不會被調用。
(2)終於搞懂struct結構體內存分配問題了,結構體中各個成員字節對齊遵循如下幾個原則:
1.結構體每一個成員相對於結構體首地址的偏移量(offset)都是(這個)成員大小的整數倍,若有須要編譯器會在成員之間加上填充字節(internal adding);
  例若有如下一個結構體 
      struct ex {
               int i;
               char t;
               int n;              
 }
   第1個成員偏移量爲0,是int型成員大小4(假設這太機器的整型長度佔4個字節)的整數倍。
   第2個成員 t 爲char型,他的大小爲1,首先假設在成員i和t之間沒有填充字節,因爲i是整型,佔4個字節那麼在沒有填充以前,第2個成員t相對於結構體的偏移量爲4,他是t成員大小1的4倍,符合此條件,因此係統在給結構體第2個成員分配內存時,不會在i和t之間填充字節以到達對齊的目的。
   當分配結構體第3個成員n時,首先發現是一個整型數據,大小爲4,沒有填充以前,n相對於結構體首地址偏移量爲:前面2個成員+填充字節=5,因此當系統發現5不是4(成員大小)的整數倍時,會在成員t以後(或者說n以前)填充3個字節,以使n的偏移量到達8而成爲4的整數倍。這樣這個結構體佔用內存狀況暫時爲4+1+3+4。
2.結構體的總大小爲結構體最寬基本類型成員大小的整數倍,若有須要編譯器會在最末一個成員以後加上填充字節(trailing padding)。
   上面的結構體內存分配之後還要看是否知足此條件,假設在最末一個成員以後不需填充字節數,那麼這個結構體的大小爲12。而ex結構體中最寬基本類型成員爲int,大小爲4,12爲4的整數倍,因此無須再在最末一個成員以後加上填充字節了。因此sizeof(ex)=12;
   若是一個結構體以下所示struct ex1{
                    int i;
                    char t;
                    int n;
                    char add;
       }
   那麼sizeof(ex1) =16;緣由就是在最後一個成員以後填充了3個字節。
3.還有一個額外的條件:結構體變量的首地址可以被其最寬基本類型成員的大小所整除;
4.對於結構體成員屬性中包含結構體變量的複合型結構體再肯定最寬基本類型成員時,應當包括複合類型成員的子成員。但在肯定複合類型成員的偏移位置時則是將複合類型做爲總體看待。
5總結出一個公式:結構體的大小等於最後一個成員的偏移量加上其大小再加上末尾的填充字節數目,即:
sizeof( struct ) = offsetof( last item ) + sizeof( last item ) + sizeof( trailing padding )
C/C++ 中已經提供了宏 offsetof 計算成員的偏移量。注意包含頭文件:C 是 <stddef.h>,C++ 是 <cstddef>。 
------------------------------------------------------------------------------------------------------------------------------------

因爲程序運行時佔用的內存過大,因此想辦法給程序瘦身。
在調試中發現結構體佔用的size居然和預想的不同,原來……編碼

看看下面講的吧,確定會不枉此看哦!

1,好比:

struct{
    short a1;
    short a2;
    short a3;
}A;
struct{
    long a1;
    short a2;
}B;
sizeof(A)=6, sizeof(B)=8,爲何?
注:sizeof(short)=2,sizeof(long)=4

由於:「成員對齊有一個重要的條件,即每一個成員按本身的方式對齊.其對齊的規則是,每一個成員按其類型的對齊參數(一般是這個類型的大小)和指定對齊參數(這裏默認是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;

能夠設置成對齊的
#pragma pack(1)
#pragma pack(push)
#pragma pack(1)
struct{
    short a1;
    short a2;
    short a3;
}A;
struct{
    long a1;
    short a2;
}B;
#pragma pack(pop)

結果爲sizeof( A)=6,sizeof( B)=6

************************

2,又如:

#pragma pack(8)
struct S1{
    char a;
    long b;
};
struct S2 {
    char c;
    struct S1 d;
    long long e;
};
#pragma pack()
sizeof(S2)結果爲24.
成員對齊有一個重要的條件,即每一個成員分別對齊.即每一個成員按本身的方式對齊.
也就是說上面雖然指定了按8字節對齊,但並非全部的成員都是以8字節對齊.其對齊的規則是,每一個成員按其類型的對齊參數(一般是這個類型的大小)和指定對齊參數(這裏是8字節)中較小的一個對齊.而且結構的長度必須爲所用過的全部對齊參數的整數倍,不夠就補空字節.
S1中,成員a是1字節默認按1字節對齊,指定對齊參數爲8,這兩個值中取1,a按1字節對齊;成員b是4個字節,默認是按4字節對齊,這時就按4字節對齊,因此sizeof(S1)應該爲8;
S2 中,c和S1中的a同樣,按1字節對齊,而d 是個結構,它是8個字節,它按什麼對齊呢?對於結構來講,它的默認對齊方式就是它的全部成員使用的對齊參數中最大的一個,S1的就是4.因此,成員d就是按4字節對齊.成員e是8個字節,它是默認按8字節對齊,和指定的同樣,因此它對到8字節的邊界上,這時, 已經使用了12個字節了,因此又添加了4個字節的空,從第16個字節開始放置成員e.這時,長度爲24,已經能夠被8(成員e按8字節對齊)整除.這樣, 一共使用了24個字節.
                               a      b
S1的內存佈局:1***, 1111,
                          c      S1.a    S1.b             e
S2的內存佈局:1***, 1***,   1111, ****11111111

這裏有三點很重要:
1.每一個成員分別按本身的方式對齊,並能最小化長度
2.複雜類型(如結構)的默認對齊方式是它最長的成員的對齊方式,這樣在成員是複雜類型時,能夠最小化長度
3.對齊後的長度必須是成員中最大的對齊參數的整數倍,這樣在處理數組時能夠保證每一項都邊界對齊

補充一下,對於數組,好比:
char a[3];這種,它的對齊方式和分別寫3個char是同樣的.也就是說它仍是按1個字節對齊.
若是寫: typedef char Array3[3];
Array3這種類型的對齊方式仍是按1個字節對齊,而不是按它的長度.
不論類型是什麼,對齊的邊界必定是1,2,4,8,16,32,64....中的一個. 


字節對齊詳解
一.爲何要對齊?
    現代計算機中內存空間都是按照byte劃分的,從理論上講彷佛對任何類型的變量的訪問能夠從任何地址開始,但實際狀況是在訪問特定類型變量的時候常常在特 定的內存地址訪問,這就須要各類類型數據按照必定的規則在空間上排列,而不是順序的一個接一個的排放,這就是對齊。
    對齊的做用和緣由:各個硬件平臺對存儲空間的處理上有很大的不一樣。一些平臺對某些特定類型的數據只能從某些特定地址開始存取。好比有些架構的CPU在訪問 一個沒有進行對齊的變量的時候會發生錯誤,那麼在這種架構下編程必須保證字節對齊.其餘平臺可能沒有這種狀況,可是最多見的是若是不按照適合其平臺要求對 數據存放進行對齊,會在存取效率上帶來損失。好比有些平臺每次讀都是從偶地址開始,若是一個int型(假設爲32位系統)若是存放在偶地址開始的地方,那 麼一個讀週期就能夠讀出這32bit,而若是存放在奇地址開始的地方,就須要2個讀週期,並對兩次讀出的結果的高低字節進行拼湊才能獲得該32bit數 據。顯然在讀取效率上降低不少。

二.字節對齊對程序的影響:
    先讓咱們看幾個例子吧(32bit,x86環境,gcc編譯器):
設結構體以下定義:
struct A
{
    int a;
    char b;
    short c;
};
struct B
{
    char b;
    int a;
    short c;
};
如今已知32位機器上各類數據類型的長度以下:
char:1(有符號無符號同)    
short:2(有符號無符號同)    
int:4(有符號無符號同)    
long:4(有符號無符號同)    
float:4    double:8
那麼上面兩個結構大小如何呢?
結果是:
sizeof(strcut A)值爲8
sizeof(struct B)的值倒是12

結構體A中包含了4字節長度的int一個,1字節長度的char一個和2字節長度的short型數據一個,B也同樣;按理說A,B大小應該都是7字節。
之因此出現上面的結果是由於編譯器要對數據成員在空間上進行對齊。上面是按照編譯器的默認設置進行對齊的結果,那麼咱們是否是能夠改變編譯器的這種默認對齊設置呢,固然能夠.例如:
#pragma pack (2) /*指定按2字節對齊*/
struct C
{
    char b;
    int a;
    short c;
};
#pragma pack () /*取消指定對齊,恢復缺省對齊*/
sizeof(struct C)值是8。
修改對齊值爲1:
#pragma pack (1) /*指定按1字節對齊*/
struct D
{
    char b;
    int a;
    short c;
};
#pragma pack () /*取消指定對齊,恢復缺省對齊*/
sizeof(struct D)值爲7。
後面咱們再講解#pragma pack()的做用.

三.編譯器是按照什麼樣的原則進行對齊的?
先讓咱們看四個重要的基本概念:
1.數據類型自身的對齊值:
對於char型數據,其自身對齊值爲1,對於short型爲2,對於int,float,double類型,其自身對齊值爲4,單位字節。
2.結構體或者類的自身對齊值:其成員中自身對齊值最大的那個值。
3.指定對齊值:#pragma pack (value)時的指定對齊值value。
4.數據成員、結構體和類的有效對齊值:自身對齊值和指定對齊值中小的那個值。
有 了這些值,咱們就能夠很方便的來討論具體數據結構的成員和其自身的對齊方式。有效對齊值N是最終用來決定數據存放地址方式的值,最重要。有效對齊N,就是 表示「對齊在N上」,也就是說該數據的"存放起始地址%N=0".而數據結構中的數據變量都是按定義的前後順序來排放的。第一個數據變量的起始地址就是數 據結構的起始地址。結構體的成員變量要對齊排放,結構體自己也要根據自身的有效對齊值圓整(就是結構體成員變量佔用總長度須要是對結構體有效對齊值的整數 倍,結合下面例子理解)。這樣就不能理解上面的幾個例子的值了。
例子分析:
分析例子B;
struct B
{
    char b;
    int a;
    short c;
};
假 設B從地址空間0x0000開始排放。該例子中沒有定義指定對齊值,在筆者環境下,該值默認爲4。第一個成員變量b的自身對齊值是1,比指定或者默認指定 對齊值4小,因此其有效對齊值爲1,因此其存放地址0x0000符合0x0000%1=0.第二個成員變量a,其自身對齊值爲4,因此有效對齊值也爲4, 因此只能存放在起始地址爲0x0004到0x0007這四個連續的字節空間中,複覈0x0004%4=0,且緊靠第一個變量。第三個變量c,自身對齊值爲 2,因此有效對齊值也是2,能夠存放在0x0008到0x0009這兩個字節空間中,符合0x0008%2=0。因此從0x0000到0x0009存放的 都是B內容。再看數據結構B的自身對齊值爲其變量中最大對齊值(這裏是b)因此就是4,因此結構體的有效對齊值也是4。根據結構體圓整的要求, 0x0009到0x0000=10字節,(10+2)%4=0。因此0x0000A到0x000B也爲結構體B所佔用。故B從0x0000到0x000B 共有12個字節,sizeof(struct B)=12;其實若是就這一個就來講它已將知足字節對齊了, 由於它的起始地址是0,所以確定是對齊的,之因此在後面補充2個字節,是由於編譯器爲了實現結構數組的存取效率,試想若是咱們定義了一個結構B的數組,那 麼第一個結構起始地址是0沒有問題,可是第二個結構呢?按照數組的定義,數組中全部元素都是緊挨着的,若是咱們不把結構的大小補充爲4的整數倍,那麼下一 個結構的起始地址將是0x0000A,這顯然不能知足結構的地址對齊了,所以咱們要把結構補充成有效對齊大小的整數倍.其實諸如:對於char型數據,其 自身對齊值爲1,對於short型爲2,對於int,float,double類型,其自身對齊值爲4,這些已有類型的自身對齊值也是基於數組考慮的,只 是由於這些類型的長度已知了,因此他們的自身對齊值也就已知了.
同理,分析上面例子C:
#pragma pack (2) /*指定按2字節對齊*/
struct C
{
    char b;
    int a;
    short c;
};
#pragma pack () /*取消指定對齊,恢復缺省對齊*/
第 一個變量b的自身對齊值爲1,指定對齊值爲2,因此,其有效對齊值爲1,假設C從0x0000開始,那麼b存放在0x0000,符合0x0000%1= 0;第二個變量,自身對齊值爲4,指定對齊值爲2,因此有效對齊值爲2,因此順序存放在0x000二、0x000三、0x000四、0x0005四個連續 字節中,符合0x0002%2=0。第三個變量c的自身對齊值爲2,因此有效對齊值爲2,順序存放
在0x000六、0x0007中,符合 0x0006%2=0。因此從0x0000到0x00007共八字節存放的是C的變量。又C的自身對齊值爲4,因此C的有效對齊值爲2。又8%2=0,C 只佔用0x0000到0x0007的八個字節。因此sizeof(struct C)=8.

四.如何修改編譯器的默認對齊值?
1.在VC IDE中,能夠這樣修改:[Project]|[Settings],c/c++選項卡Category的Code Generation選項的Struct Member Alignment中修改,默認是8字節。
2.在編碼時,能夠這樣動態修改:#pragma pack .注意:是pragma而不是progma.

五.針對字節對齊,咱們在編程中如何考慮?

    若是在編程的時候要考慮節約空間的話,那麼咱們只須要假定結構的首地址是0,而後各個變量按照上面的原則進行排列便可,基本的原則就是把結構中的變量按照 類型大小從小到大聲明,儘可能減小中間的填補空間.還有一種就是爲了以空間換取時間的效率,咱們顯示的進行填補空間進行對齊,好比:有一種使用空間換時間作 法是顯式的插入reserved成員:
struct A{
    char a;
    char reserved[3];//使用空間換時間
    int b;
}

reserved成員對咱們的程序沒有什麼意義,它只是起到填補空間以達到字節對齊的目的,固然即便不加這個成員一般編譯器也會給咱們自動填補對齊,咱們本身加上它只是起到顯式的提醒做用.

 
Over!更多內容,參照此文: https://www.ibm.com/developerworks/library/pa-dalign/
內存對齊的具體規則,參照此篇博客: http://blog.csdn.net/liu1064782986/article/details/7600979
相關文章
相關標籤/搜索