天天看點

結構體位元組對齊

結構體位元組對齊

      在用sizeof運算符求算某結構體所占空間時,并不是簡單地将結構體中所有元素各自占的空間相加,這裡涉及到記憶體位元組對齊的問題。從理論上講,對于任何 變量的通路都可以從任何位址開始通路,但是事實上不是如此,實際上通路特定類型的變量隻能在特定的位址通路,這就需要各個變量在空間上按一定的規則排列, 而不是簡單地順序排列,這就是記憶體對齊。

     計算結構變量的大小必須讨論資料對齊的問題。為了使CPU存取的速度最快(這同CPU取數操作有關),c++在處理資料時經常把結構變量中的成員的大小按照4或8的倍數計算,這就叫資料對齊(data alignment)。這樣做可能會浪費一些記憶體,但在理論上CPU速度快了。

       記憶體對齊的原因:

      1)某些平台隻能在特定的位址處通路特定類型的資料;

      2)提高存取資料的速度。比如有的平台每次都是從偶位址處讀取資料,對于一個int型的變量,若從偶位址單元處存放,則隻需一個讀取周期即可讀取該變量;但是若從奇位址單元處存放,則需要2個讀取周期讀取該變量。

  在C99标準中,對于記憶體對齊的細節沒有作過多的描述,具體的實作交由編譯器去處理,是以在不同的編譯環境下,記憶體對齊可能略有不同,但是對齊的最基本原則是一緻的。

     對于結構體的位元組對齊主要有下面兩點:

      1)結構體每個成員相對結構體首位址的偏移量(offset)是對齊參數(這句話中的對齊參數是 取每個變量自身對齊參數和系統預設對齊參數#pragma pack(n)中較小的一個)的整數倍,如有需要會在成員之間填充位元組。編譯器在為結構體成員開辟空間時,首先 檢查預開辟空間的位址相對于結構體首位址的偏移量是否為對齊參數的整數倍,若是,則存放該成員;若不是,則填充若幹位元組,以達到整數倍的要求。

      2)結構體變量所占空間的大小是對齊參數(它是取結構體中所有變量的對齊參數的最大值和系統 預設對齊參數#pragma pack(n)比較,較小者作為對齊參數)大小的整數倍。如有需要會在最後一個成員末尾填充若幹位元組使得所占空間大小是對齊參數大小的整數倍。

   注意:在看這兩條原則之前,先了解一下對齊參數這個概念。對于每個變量,它自身有對齊參數,這個自身對齊參數在不同編譯環境下不同。下面列舉的是兩種最常見的編譯環境下各種類型變量的自身對齊參數

結構體位元組對齊

1.          記憶體對齊與編譯器設定有關,首先要搞清編譯器這個預設值是多少

2.          如果不想編譯器預設的話,可以通過#pragma pack(n)來指定按照n對齊

3.          每個結構體變量對齊,如果對齊參數n(編譯器預設或者通過pragma指定)大于該變量所占位元組數(m),那麼就按照m對齊,記憶體偏移後的位址是m的倍數,否則是按照n對齊,記憶體偏移後的位址是n的倍數。也就是最小化長度規則

4.          結構體總大小: 對齊後的長度必須是成員中最大的對齊參數的整數倍。最大對齊參數是從第三步得到的。

5.          補充:如果結構體A中還要結構體B,那麼B的對齊方式是選它裡面最長的成員的對齊方式

是以計算結構體大小要走三步,首先确定是目前程式按照幾對齊(參照1,2點),接着計算每個結構體變量的大小和偏移(參照3,5),最後計算結構體總大小(參照4)。

   從上面可以發現,在windows(32)/VC6.0下各種類型的變量的自身對齊參數就是該類型變量所占位元組數的大小,而在 linux(32)/GCC下double類型的變量自身對齊參數是4,是因為linux(32)/GCC下如果該類型變量的長度沒有超過CPU的字長, 則以該類型變量的長度作為自身對齊參數,如果該類型變量的長度超過CPU字長,則自身對齊參數為CPU字長,而32位系統其CPU字長是4,是以 linux(32)/GCC下double類型的變量自身對齊參數是4,如果是在Linux(64)下,則double類型的自身對齊參數是8。

   除了變量的自身對齊參數外,還有一個對齊參數,就是每個編譯器預設的對齊參數#pragma pack(n),這個值可以通過代碼去設定,如果沒有設定,則取系統的預設值。在windows(32)/VC6.0下,n的取值可以為1、2、4、8, 預設情況下為8。在linux(32)/GCC下,n的取值隻能為1、2、4,預設情況下為4。注意像DEV-CPP、MinGW等在windows下n 的取值和VC的相同。

  了解了這2個概念之後,可以了解上面2條原則了。對于第一條原則,每個變量相對于結構體的首位址的偏移量必須是對齊參數的整數倍,這句話中的對齊參數是取每個變量自身對齊參數和系統預設對齊參數#pragma pack(n)中較小的一個。舉個簡單的例子,比如在結構體A中有變量int a,a的自身對齊參數為4(環境為windows/vc),而VC預設的對齊參數為8,取較小者,則對于a,它相對于結構體A的起始位址的偏移量必須是4 的倍數。

  對于第二條原則,結構體變量所占空間的大小是對齊參數的整數倍。這句話中的對齊參數有點複雜,它是取結構體中所有變量的對齊參數的最大值和系統 預設對齊參數#pragma pack(n)比較,較小者作為對齊參數。舉 個例子假如在結構體A中先後定義了兩個變量int a;double b;對于變量a,它的自身對齊參數為4,而#pragma pack(n)值預設為8,則a的對齊參數為4;b的自身對齊參數為8,而#pragma pack(n)的預設值為8,則b的對齊參數為8。由于a的最終對齊參數為4,b的最終對齊參數為8,那麼兩者較大者是8,然後再拿8和#pragma pack(n)作比較,取較小者作為對齊參數,也就是8,即意味着結構體最終的大小必須能被8整除。

下面是測試例子:

注意:以下例子的測試結果均在windows(32)/VC下測試的,其預設對齊參數為8

#include <iostream>

using namespace std;

//#pragma pack(4)    //設定4位元組對齊

//#pragma pack()     //取消4位元組對齊

typedef struct node1

{

    int a;

    char b;

    short c;

}S1;

typedef struct node2

    char a;

    int b;

}S2;

typedef struct node3

    short b;

    static int c;

}S3;

typedef struct node4

    bool a;

    S1 s1;

}S4;

typedef struct node5

    double b;

    int c;

}S5;

int main(int argc, char *argv[])

    cout<<sizeof(char)<<" "<<sizeof(short)<<" "<<sizeof(int)<<" "<<sizeof(float)<<" "<<sizeof(double)<<endl;

    S2 s2;

    S3 s3;

    S4 s4;

    S5 s5;

    cout<<sizeof(s1)<<" "<<sizeof(s2)<<" "<<sizeof(s3)<<" "<<sizeof(s4)<<" "<<sizeof(s5)<<endl;

    return 0;

}

下面解釋一下其中的幾個結構體位元組配置設定的情況

比如對于node2

 sizeof(S2)=12;

  對于變量a,它的自身對齊參數為1,#pragma pack(n)預設值為8,則最終a的對齊參數為1,為其配置設定1位元組的空間,它相對于結構體起始位址的偏移量為0,能被4整除;

  對于變量b,它的自身對齊參數為4,#pragma pack(n)預設值為8,則最終b的對齊參數為4,接下來的位址相對于結構體的起始位址的偏移量為1,1不能夠整除4,是以需要在a後面填充3位元組使得偏移量達到4,然後再為b配置設定4位元組的空間;

  對于變量c,它的自身對齊參數為2,#pragma pack(n)預設值為8,則最終c的對齊參數為2,而接下來的位址相對于結構體的起始位址的偏移量為8,能整除2,是以直接為c配置設定2位元組的空間。

  此時結構體所占的位元組數為1+3+4+2=10位元組

  最後由于a,b,c的最終對齊參數分别為1,4,2,最大為4,#pragma pack(n)的預設值為8,則結構體變量最後的大小必須能被4整除。而10不能夠整除4,是以需要在後面填充2位元組達到12位元組。其存儲如下:

  |char|----|----|----|  4位元組

    |--------int--------|  4位元組

    |--short--|----|----|  4位元組

  總共占12個位元組

對于node3,含有靜态資料成員 

  則sizeof(S3)=8.這裡結構體中包含靜态資料成員,而靜态資料成員的存放位置(靜态變量是存放在全局資料區的,而sizeof計算棧中配置設定的大小,是不會計算在内的)與結構體執行個體的存儲位址無關(注意隻有在C++中結構體中才能含有靜态資料成員,而C中結構體中是不允許含有靜态資料成員的)。其在記憶體中存儲方式如下:

  |--------int--------|   4位元組

  |--short-|----|----|    4位元組

  而變量c是單獨存放在靜态資料區的,是以用siezof計算其大小時沒有将c所占的空間計算進來。

而對于node5,裡面含有結構體變量

結構體位元組對齊
結構體位元組對齊

sizeof(S5)=32。

  對于變量a,其自身對齊參數為1,#pragma pack(n)為8,則a的最終對齊參數為1,為它配置設定1位元組的空間,它相對于結構體起始位址的偏移量為0,能被1整除;

  對于s1,它的自身對齊參數為4(對于結構體變量,它的自身對齊參數為它裡面各個變量最終對齊參數的最大值),#pragma pack(n)為8,是以s1的最終對齊參數為4,接下來的位址相對于結構體起始位址的偏移量為1,不能被4整除,是以需要在a後面填充3位元組達到4,為 其配置設定8位元組的空間;

  對于變量b,它的自身對齊參數為8,#pragma pack(n)的預設值為8,則b的最終對齊參數為8,接下來的位址相對于結構體起始位址的偏移量為12,不能被8整除,是以需要在s1後面填充4位元組達到16,再為b配置設定8位元組的空間;

  對于變量c,它的自身對齊參數為4,#pragma pack(n)的預設值為8,則c的最終對齊參數為4,接下來相對于結構體其實位址的偏移量為24,能夠被4整除,是以直接為c配置設定4位元組的空間。

  此時結構體所占位元組數為1+3+8+4+8+4=28位元組。

  對于整個結構體來說,各個變量的最終對齊參數為1,4,8,4,最大值為8,#pragma pack(n)預設值為8,是以最終結構體的大小必須是8的倍數,是以需要在最後面填充4位元組達到32位元組。其存儲如下:

   s5的記憶體配置設定應該如下:

|--------bool--------| 4位元組

|---------s1---------| 4位元組

|---------------------| 空出

|--------double-----| 8位元組

|----int----|---------| 8位元組

  另外可以顯示地在程式中使用#pragma pack(n)來設定系統預設的對齊參數,在顯示設定之後,則以設定的值作為标準,其它的和上面所講的類似,就不再贅述了,讀者可以自行上機試驗一下。如果需要取消設定,可以用#pragma pack()來取消。

   結構體的長度一定是最長的資料元素的整數倍。

   CPU的優化規則大緻原則是這樣的:對于n位元組的元素(n=2,4,8,...),它的首位址能被n整除,才能獲得最好的性能。

   出這類題并不在于考查了解語言本身和編譯器,而在于應聘者對計算機底層機制的了解和設計程式的原則。也就是說,如果讓你設計編譯器,你将怎樣解決記憶體對齊的為問題。

本文轉自夏雪冬日部落格園部落格,原文連結:http://www.cnblogs.com/heyonggang/archive/2012/12/11/2812304.html,如需轉載請自行聯系原作者

繼續閱讀