細說結構字節對齊

 1. 概述
    本文討論了結構的自然邊界對齊,在缺省情況下,c編譯器爲每一個變量或數據單元按其自然邊界對齊條件分配空間。
    但可以通過四種方法來更改C編譯器的缺省字節對齊方式,即可以指定邊界對齊。
    
    在閱讀完本文檔後,將會更深入地瞭解一個結構的sizeof到底應當是多少。

2. 自然邊界對齊
    在C語言中,結構是一種複合數據類型,其構成元素既可以是基本數據類型(如int、long、float等)的變量,也可以是一些複合數據類型(如數組、結構、聯合等)的數據單元。

    結構字節對齊有以下幾個特點:
    1. 對於結構體,編譯器會自動進行成員變量的對齊,以提高運算效率。
       缺省情況下,編譯器爲結構的每個成員按其自然邊界對齊( natural alignment)條件分配空間。
       自然邊界對齊即爲默認對齊方式。

    2. 各個成員按照它們被聲明的順序在內存中順序存儲,第一個成員的地址和整個結構的地址相同。

    3. 結構整體的默認對齊方式就是它的所有成員使用的對齊參數中最大的一個。

    4. 結構整體長度的計算必須取所用過的所有對齊參數的整數倍,不夠補空字節;
       也就是取所用過的所有對齊參數中最大的那個值的整數倍,因爲對齊參數都是2的n次方;
       這樣在處理數組時可以保證每一項都邊界對齊;

    例如,下面的結構各成員空間分配情況:
    struct test
    {
        char  x1;
        short x2;
        float x3;
        char  x4;
    };
    結構的第一個成員x1,其偏移地址爲0,佔據了第1個字節。
    第二個成員x2爲short類型,其起始地址必須2字節邊界對齊,因此,編譯器在x2和x1之間填充了一個空字節。
    結構的第三個成員x3和第四個成員x4恰好落在其自然邊界對齊地址上,在它們前面不需要額外的填充字節。
    在test結構中,成員x3要求4字節邊界對齊,是該結構所有成員中要求的最大邊界對齊單元,因而test結構的自然邊界對齊條件爲4字節,編譯器在成員x4後面填充了3個空字節。整個結構所佔據空間爲12字節。

3. 指定邊界對齊
   在缺省情況下,c編譯器爲每一個變量或數據單元按其自然邊界對齊條件分配空間;但可以通過下面四種方法來更改C編譯器的缺省字節對齊方式:

方法1: 使用#pragma pack
     #pragma pack說明:
     1. pack提供數據聲明級別的控制,對定義不起作用;
     2. 調用pack時不指定參數,將恢復C編譯器的缺省字節對齊方式,即使用僞指令#pragma pack()將取消自定義字節對齊方式;
     3. 一旦改變數據類型的alignment,直接效果就是佔用memory的減少,但是performance可能會下降;
   
    #pragma pack語法詳細說明:
    1. show:可選參數;顯示當前packing aligment的字節數,以warning message的形式被顯示;
    2. push:可選參數;將當前指定的packing alignment數值進行壓棧操作,這裏的棧是the internal compiler stack,同時設置當前的packing alignment爲n;如果n沒有指定,則將當前的packing alignment數值壓棧;
    3. pop:可選參數;從internal compiler stack中刪除最頂端的record;如果沒有指定n,則當前棧頂record即爲新的packing alignment數值;如果指定了n,則n將成爲新的packing aligment數值;如果指定了identifier,則internal compiler stack中的record都將被pop直到identifier被找到,然後pop出identitier,同時設置packing alignment數值爲當前棧頂的record;如果指定的identifier並不存在於internal compiler stack,則pop操作被忽略;
    4. identifier:可選參數;當同push一起使用時,賦予當前被壓入棧中的record一個名稱;當同pop一起使用時,從internal compiler stack中pop出所有的record直到identifier被pop出,如果identifier沒有被找到,則忽略pop操作;
    5. n:可選參數;指定packing的數值,以字節爲單位;缺省數值是8,合法的數值分別是1、2、4、8、16。

    #pragma pack規定的對齊長度,實際使用的規則是:結構,聯合或者類的數據成員,第一個放在偏移爲0的地方,以後每個數據成員的對齊,按照#pragma pack指定的數值和這個數據成員自身長度中比較小的值來對齊。
    也就是說,當#pragma pack的值等於或超過所有數據成員長度的時候,這個值的大小將不產生任何效果。
    而結構整體的對齊,則按照結構體中size最大的數據成員和#pragma pack指定值之間較小的值來對齊。

    -------------------------------------------
    例1:
    #pragma pack(2)
    struct TestA
  {
  public:
    int   a; // 第一個成員,放在[0,3]偏移的位置。
    char  b; // 第二個成員sizeof(char)=1,#pragma pack(2), 取小值也就是1,所以這個成員按1字節對齊,放在偏移[4]的位置。
    short c; // 第三個成員sizeof(short)=2, #pragma pack(2),取小值也就是2,所以這個成員按2字節對齊,所以放在偏移[6,7]的位置。
    char  d; // 第四個成員sizeof(short)=1, #pragma pack(2), 取小值也就是1,所以這個成員按1字節對齊,放在[8]的位置。
  };
    #pragma pack()
    struct TestA中size最大的數據成員(4),#pragma pack(2), 取小值也就是2,所以sizeof(TestA)應當按照2來對齊,爲10。

    -------------------------------------------
    例2:
    #pragma pack(4)
    struct TestB
  {
  public:
    int   a; // 第一個成員,放在[0,3]偏移的位置。
    char  b; // 第二個成員sizeof(char)=1,#pragma pack(4), 取小值也就是1,所以這個成員按1字節對齊,放在偏移[4]的位置。
    short c; // 第三個成員sizeof(short)=2, #pragma pack(4),取小值也就是2,所以這個成員按2字節對齊,所以放在偏移[6,7]的位置。
    char  d; // 第四個成員sizeof(short)=1, #pragma pack(4), 取小值也就是1,所以這個成員按1字節對齊,放在[8]的位置。
  };
    #pragma pack()
    struct TestB中size最大的數據成員(4),#pragma pack(4),  取小值也就是4,所以sizeof(TestB)應當按照4來對齊,爲12。

    -------------------------------------------
    例3:
    #pragma pack(8)
    struct s1
    {
        short a; // 第一個成員,放在[0, 1]偏移的位置。
        long  b; // 第二個成員sizeof(long)=4, #pragma pack(8), 取小值也就是4,所以這個成員按4字節對齊,放在偏移[4~7]的位置。
    };

    struct s2
    {
        char      c; // 第一個成員,放在[0]偏移的位置。
        struct s1 d; // 第二個成員爲struct s1,其對齊方式是它的所有成員使用的對齊參數中最大的一個,即4。
                     // 所以第二個成員d按4字節對齊,由於sizeof(d)=8, 放在偏移[4~11]的位置。
        long long e; // 第三個成員sizeof(long long)=8, #pragma pack(8), 取小值也就是8,所以這個成員按8字節對齊,放在偏移[16~23]的位置。
    };
    #pragma pack()

    問:
    1. sizeof(struct s2) = ?
    2. s2的c後面空了幾個字節接着是d?
   
    答案1:
        struct s1中size最大的數據成員(4),#pragma pack(8),取小值也就是4,所以sizeof(struct s1)應當按照4來對齊,爲8。
        struct s2中size最大的數據成員(8),#pragma pack(8),取小值也就是8,所以sizeof(struct s2)應當按照8來對齊,爲24。
    答案2:
        s2的c後面空了3個字節接着是d。
   
方法2: 使用__attribute((aligned (alignment)))
     aligned(alignment)屬性作用於變量或結構成員,參數alignment表示最小的對齊字節數。
     例如:
     int x __attribute__ ((aligned (16))) = 0;
     使編譯器爲全局變量x分配空間在16字節邊界。
    
     例如:創建一個以8字節爲邊界對齊的兩個整數,可以寫爲:
     struct foo
     {
        int x[2] __attribute__ ((aligned (8)));
     };

     前面兩個例子中,指定參數alignment告訴編譯器作用於變量或結構成員。
     但也可以不指定參數alignment,讓編譯器根據爲編譯的目標機採用最大最有益的方式對齊。
     例如:
          short array[3] __attribute__ ((aligned));
     一旦在aligned()屬性中不指定參數,編譯器會自動將變量或結構成員參數alignment設置爲目標機上曾經使用的數據類型中最大的alignment,
     這樣可以使copy操作效率更高。
    
     aligned屬性只能用於增加alignment; 可以使用packed屬性來減小alignment。
    
     注意:aligned屬性應用效果受到鏈接器的限制。在許多系統中,鏈接器將變量對齊只能設置到某個最大值。
     假如使用的鏈接器將變量對齊最大隻能設置爲8字節,那麼指定aligned(16)屬性只能提供8字節的邊界對齊。
     需要參考具體使用的鏈接器相關文檔。

     舉例:    
     struct A{
        char               a;
        int                b;
        unsigned short     c;
        long               d;
        unsigned long long e;
        char               f;
    };
    因爲什麼也沒有跟,所以採用默認處理方式。其結果是與採用__attribute__((aligned(4)))相同。
    sizeof(struct A) = 4(a, 1-->4)+ 4 + 4(c, 2-->4) + 4 + 8 + 4(f, 1-->4) = 28。

     struct B{
        char               a;
        int                b;
        unsigned short     c;
        long               d;
        unsigned long long e;
        char               f;
    }__attribute__((aligned));
    在struct B中,aligned沒有參數,表示“讓編譯器根據目標機制採用最大最有益的方式對齊"。
    當然,最有益應該是運行效率最高吧,呵呵。其結果是與採用__attribute__((aligned(8)))相同。
    sizeof(struct B) = 8(1+4+2 ,即a, b, c)+ 8(d, 4-->8) + 8 + 8(f, 1-->8) = 32。
   
     struct C{
        char               a;
        int                b;
        unsigned short     c;
        long               d;
        unsigned long long e;
        char               f;
    }__attribute__((aligned(1)));
    在struct C中,試圖使用__attribute__((aligned(1)))來使用1個字節方式的對齊,不過並未如願,仍然採用了默認4個字節的對齊方式。

方法3: 使用__attribute__ ((packed))取消結構在編譯過程中的優化對齊
     __attribute__ ((packed))作用於結構成員,表示該成員與前一個結構成員之間沒有空洞。
     舉例:
     struct foo
     {
        char a;
        int x[2] __attribute__ ((packed));
     };
     這裏packed屬性作用於成員x,因而在結構成員a後沒有空洞,而是立即緊跟着成員x。

     __attribute__ ((packed))作用於整個結構,等同於爲結構中的每個成員指定__attribute__ ((packed)),也與結構前後利用#pragma pack(1)等效。
     舉例:
     struct F{
        char               a;
        int                b;
        unsigned short     c;
        long               d;
        unsigned long long e;
        char               f;
    }__attribute__((packed));
    sizeof(struct F) = 1 + 4 + 2 + 4 + 8 + 1 = 20。
   
    注意:在使用了packed屬性限定之後,GCC編譯器將用字節存取命令(ARM中爲LDRB或STRB指令)來訪問該結構成員,
    而不是按照自然邊界對齊方式來訪問結構成員,可參見。
   
方法4: GCC編譯選項中使用-fpack-struct[=n]
       如果沒有指定n, 則去除所有結構中的空洞(注意這裏會影響到所有的結構),即編譯器不能在成員之間填充邊界對齊的空字節。
       如果指定n, 則n表示maximum alignment (that is, objects with default alignment requirements larger than this will be output potentially unaligned at the next fitting location)。
      
       但通常不應當使用該選項,因爲這會使訪問結構成員的效率降低,代碼量增大(通常會增加1/3左右,當Flash空間很有限時就要認真考慮了),
       而且使生成的代碼與沒有使用該編譯選項的系統庫不兼容。
          
4. 補充
   對於諸如char a[3];這種數組,它的對齊方式和分別寫3個char是一樣的,也就是說它還是按1個字節對齊。
   如果寫爲typedef char Array3[3];,則Array3這種類型的對齊方式還是按1個字節對齊,而不是按它的長度。
   不論類型是什麼,對齊的邊界一定是1,2,4,8,16,32,64....中的一個.
  
5. 參考資料
   [1] ARM體系結構下數據訪問時的對齊問題.txt
   [2] GCC 4.3.2 Manual, [url]http://gcc.gnu.org/onlinedocs/[/url]
   [3] ARM嵌入式軟件編程經驗談, 華清遠見科技信息有限公司, [url]www.realview.com.cn/shoppic/iq-006/P22-23-ARM[/url]嵌入式軟件編程經驗談.pdf

本文出自 “kapu ” 博客,請務必保留此出處http://kapok.blog.51cto.com/517862/127218

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章