內存對齊

內存對齊


1.   概念


  對齊跟數據在內存中的位置有關。若是一個變量的內存地址正好位於它長度的整數倍,他就被稱作天然對齊。好比在32位cpu下,假設一個整型變量的地址爲0x00000004,那它就是天然對齊的。c++


1.1爲何要字節對齊

 須要字節對齊的根本緣由在於CPU訪問數據的效率問題。假設上面整型變量的地址不是天然對齊,好比爲0x00000002,則CPU若是取它的值的話須要訪問兩次內存,第一次取從0x00000002-0x00000003的一個short,第二次取從0x00000004-0x00000005的一個 short而後組合獲得所要的數據,若是變量在0x00000003地址上的話則要訪問三次內存,第一次爲char,第二次爲short,第三次爲 char,而後組合獲得整型數據。而若是變量在天然對齊位置上,則只要一次就能夠取出數據。一些系統對對齊要求很是嚴格,好比sparc系統,若是取未對齊的數據會發生錯誤,舉個例:編程


  char ch[8];數組

  char *p = &ch[1];數據結構

  int i = *(int *)p;架構


 運行時會報segment error,而在x86上就不會出現錯誤,只是效率降低。ide

1.2正確處理字節對齊

  對於標準數據類型,它的地址只要是它的長度的整數倍就好了,而非標準數據類型按下面的原則對齊:優化


  數組 :按照基本數據類型對齊,第一個對齊了後面的天然也就對齊了。 編碼

  聯合 :按其包含的長度最大的數據類型對齊。 spa

 結構體: 結構體中每一個數據類型都要對齊。設計

  好比有以下一個結構體:


  struct stu{

   char sex;

   int length;

   char name[10];

  };

  struct stu my_stu;



  因爲在x86下,GCC默認按4字節對齊,它會在sex後面跟name後面分別填充三個和兩個字節使length和整個結構體對齊。因而咱們sizeof(my_stu)會獲得長度爲20,而不是15.


1.3__attribute__選項


  咱們能夠按照本身設定的對齊大小來編譯程序,GNU使用__attribute__選項來設置,好比咱們想讓剛纔的結構按一字節對齊,咱們能夠這樣定義結構體


  struct stu{

   char sex;

   int length;

   char name[10];

  }__attribute__ ((aligned (1)));


  struct stu my_stu;



  則sizeof(my_stu)能夠獲得大小爲15。


  上面的定義等同於


  struct stu{

   char sex;

   int length;

   char name[10];

  }__attribute__ ((packed));

  struct stu my_stu;



  __attribute__((packed))得變量或者結構體成員使用最小的對齊方式,即對變量是一字節對齊,對域(field)是位對齊.


1.4何時須要設置對齊


  在設計不一樣CPU下的通訊協議時,或者編寫硬件驅動程序時寄存器的結構這兩個地方都須要按一字節對齊。即便看起來原本就天然對齊的也要使其對齊,以避免不一樣的編譯器生成的代碼不同.


2. 快速理解

2.1. 什麼是字節對齊

在 C語言中,結構是一種複合數據類型,其構成元素既能夠是基本數據類型(如int、long、float等)的變量,也能夠是一些複合數據類型(如數組、結構、聯合等)的數據單元。在結構中,編譯器爲結構的每一個成員按其天然邊界(alignment)分配空間。各個成員按照它們被聲明的順序在內存中順序存儲,第一個成員的地址和整個結構的地址相同。

爲了使CPU可以對變量進行快速的訪問,變量的起始地址應該具備某些特性,即所謂的」對齊」. 好比4字節的int型,其起始地址應該位於4字節的邊界上,即起始地址可以被4整除.


2.2. 字節對齊有什麼做用

字節對齊的做用不只是便於cpu快速訪問,同時合理的利用字節對齊能夠有效地節省存儲空間。

對於32位機來講,4字節對齊可以使cpu訪問速度提升,好比說一個long類型的變量,若是跨越了4字節邊界存儲,那麼cpu要讀取兩次,這樣效率就低了。可是在32位機中使用1字節或者2字節對齊,反而會使變量訪問速度下降。因此這要考慮處理器類型,另外還得考慮編譯器的類型。在vc中默認是4字節對齊的,GNU gcc 也是默認4字節對齊。


2.3. 更改C編譯器的缺省字節對齊方式

在缺省狀況下,C編譯器爲每個變量或是數據單元按其天然對界條件分配空間。通常地,能夠經過下面的方法來改變缺省的對界條件:

使用僞指令#pragma pack (n),C編譯器將按照n個字節對齊。

使用僞指令#pragma pack (),取消自定義字節對齊方式。

另外,還有以下的一種方式:

__attribute((aligned (n))),讓所做用的結構成員對齊在n字節天然邊界上。若是結構中有成員的長度大於n,則按照最大成員的長度來對齊。

__attribute__ ((packed)),取消結構在編譯過程當中的優化對齊,按照實際佔用字節數進行對齊。


2.4. 舉例說明


例1

struct test

{

char x1;

short x2;

float x3;

char x4;

};

因爲編譯器默認狀況下會對這個struct做天然邊界(有人說「天然對界」我以爲邊界更順口)對齊,結構的第一個成員x1,其偏移地址爲0,佔據了第1個字節。第二個成員x2爲short類型,其起始地址必須2字節對界,所以,編譯器在x2和x1之間填充了一個空字節。結構的第三個成員x3和第四個成員x4剛好落在其天然邊界地址上,在它們前面不須要額外的填充字節。在test結構中,成員x3要求4字節對界,是該結構全部成員中要求的最大邊界單元,於是test結構的天然對界條件爲4字節,編譯器在成員x4後面填充了3個空字節。整個結構所佔據空間爲 12字節。


例2

#pragma pack(1) //讓編譯器對這個結構做1字節對齊

struct test

{

char x1;

short x2;

float x3;

char x4;

};

#pragma pack() //取消1字節對齊,恢復爲默認4字節對齊

這時候sizeof(struct test)的值爲8。


例3

#define GNUC_PACKED __attribute__((packed))

struct PACKED test

{

char x1;

short x2;

float x3;

char x4;

}GNUC_PACKED;

這時候sizeof(struct test)的值仍爲8。

3深刻理解

3.1什麼是字節對齊


現代計算機中內存空間都是按照byte劃分的,從理論上講彷佛對任何類型的變量的訪問能夠從任何地址開始,但實際狀況是在訪問特定類型變量的時候常常在特定的內存地址訪問,這就須要各類類型數據按照必定的規則在空間上排列,而不是順序的一個接一個的排放,這就是對齊。


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

3.2字節對齊對程序的影響:

先讓咱們看幾個例子吧(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()的做用.

3.3編譯器是按照什麼樣的原則進行對齊的

先讓咱們看四個重要的基本概念:


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.

3.4如何修改編譯器的默認對齊值

1.在VC IDE中,能夠這樣修改:[Project]|[Settings],c/c++選項卡Category的CodeGeneration選項的Struct Member Alignment中修改,默認是8字節。


2.在編碼時,能夠這樣動態修改:#pragmapack .注意:是pragma而不是progma.

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

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

           struct A{

              char a;

              char reserved[3];//使用空間換時間

              int b;

}

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

3.6字節對齊可能帶來的隱患

代碼中關於對齊的隱患,不少是隱式的。好比在強制類型轉換的時候。例如:


unsigned int i = 0x12345678;

unsigned char *p=NULL;

unsigned short *p1=NULL;

p=&i;

*p=0x00;

p1=(unsigned short *)(p+1);

*p1=0x0000;


最後兩句代碼,從奇數邊界去訪問unsignedshort型變量,顯然不符合對齊的規定。

在x86上,相似的操做只會影響效率,可是在MIPS或者sparc上,可能就是一個error,由於它們要求必須字節對齊.


3.7如何查找與字節對齊方面的問題

若是出現對齊或者賦值問題首先查看

1. 編譯器的big little端設置

2. 看這種體系自己是否支持非對齊訪問

3. 若是支持看設置了對齊與否,若是沒有則看訪問時須要加某些特殊的修飾來標誌其特殊訪問操做


舉例:


#include <stdio.h>

main()

{

struct A {

  int a;

  char b;

  short c;

};


struct B {

  char b;

  int a;

  short c;

};


#pragma pack (2) /*指定按2字節對齊*/

struct C {

  char b;

  int a;

  short c;

};

#pragma pack () /*取消指定對齊,恢復缺省對齊*/




#pragma pack (1) /*指定按1字節對齊*/

struct D {

  char b;

  int a;

  short c;

};

#pragma pack ()/*取消指定對齊,恢復缺省對齊*/


int s1=sizeof(struct A);

int s2=sizeof(struct B);

int s3=sizeof(struct C);

int s4=sizeof(struct D);

printf("%d\n",s1);

printf("%d\n",s2);

printf("%d\n",s3);

printf("%d\n",s4);

}


輸出:

8

12

8

7


修改代碼:

struct A {
  // int a;
   char b;
   short c;
};

struct B {
   char b;
  // int a;
   short c;
};

輸出:

4

4

輸出都是4,說明以前的int影響對齊!

Center

相關文章
相關標籤/搜索