內存對齊

對齊原則:
原則1:數據成員對齊規則:結構(struct)(或聯合(union))的數據成員,第一個數據成員放在offset爲0的地方,以後每個數據成員的對齊按照#pragma pack指定的數值和這個數據成員自身長度中,比較小的那個進行。
原則2:結構(或聯合)的整體對齊規則:在數據成員完成各自對齊之後,結構(或聯合)本身也要進行對齊,對齊將按照#pragma pack指定的數值和結構(或聯合)最大數據成員長度中,比較小的那個進行。
原則3:結構體作爲成員:如果一個結構裏有某些結構體成員,則結構體成員要從其內部最大元素大小的整數倍地址開始存儲。
默認對齊值:
Linux 默認#pragma pack(4)
window 默認#pragma pack(8)
注:可以通過預編譯命令#pragma pack(n),n=1,2,4,8,16來改變這一系數,其中的n就是指定的“對齊係數”。
例一:一字節對齊
第一步: 成員數據對齊
#pragma pack(1)
struct AA {
    int a;   //長度4 > 1 按1對齊;偏移量爲0;存放位置區間[0,3]
    char b;  //長度1 = 1 按1對齊;偏移量爲4;存放位置區間[4]
    short c; //長度2 > 1 按1對齊;偏移量爲5;存放位置區間[5,6]
    char d;  //長度1 = 1 按1對齊;偏移量爲6;存放位置區間[7]
    //整體存放在[0~7]位置區間中,共八個字節。
};
#pragma pack()

第二步: 整體對齊
整體對齊係數 = min((max(int,short,char), 1) = 1,所以不需要再進行整體對齊。整體大小就爲8。
 


例二:二字節對齊
第一步: 成員數據對齊
#pragma pack(2)
struct AA {
    int a;   //長度4 > 2 按2對齊;偏移量爲0;存放位置區間[0,3]
    char b;  //長度1 < 2 按1對齊;偏移量爲4;存放位置區間[4]
    short c; //長度2 = 2 按2對齊;偏移量要提升到2的倍數6;存放位置區間[6,7]
    char d;  //長度1 < 2 按1對齊;偏移量爲7;存放位置區間[8];共九個字節
};
#pragma pack()

第二步: 整體對齊
整體對齊係數 = min((max(int,short,char), 2) = 2,將9提升到2的倍數,則爲10.所以最終結果爲10個字節。
 


例三:四字節對齊
第一步: 成員數據對齊
#pragma pack(4)
struct AA {
    int a;   //長度4 = 4 按4對齊;偏移量爲0;存放位置區間[0,3]
    char b;  //長度1 < 4 按1對齊;偏移量爲4;存放位置區間[4]
    short c; //長度2 < 4 按2對齊;偏移量要提升到2的倍數6;存放位置區間[6,7]
    char d;  //長度1 < 4 按1對齊;偏移量爲7;存放位置區間[8];總大小爲9
};
#pragma pack()

第二步: 整體對齊
整體對齊係數 = min((max(int,short,char), 4) = 4,將9提升到4的倍數,則爲12.所以最終結果爲12個字節。

例三:八字節對齊
第一步: 成員數據對齊
#pragma pack(8)
struct AA {
    int a;   //長度4 < 8 按4對齊;偏移量爲0;存放位置區間[0,3]
    char b;  //長度1 < 8 按1對齊;偏移量爲4;存放位置區間[4]
    short c; //長度2 < 8 按2對齊;偏移量要提升到2的倍數6;存放位置區間[6,7]
    char d;  //長度1 < 8 按1對齊;偏移量爲7;存放位置區間[8],總大小爲9
};
#pragma pack()

第二步: 整體對齊
整體對齊係數 = min((max(int,short,char), 8) = 4,將9提升到4的倍數,則爲12.所以最終結果爲12個字節。圖示如上。
注:可以通過stddef.h庫中的offsetof宏來查看對應結構體元素的偏移量。
例四:結構體中包含結構體的運算
整體計算過程如下
struct EE
{
    int a;      //長度4 < 8 按4對齊;偏移量爲0;存放位置區間[0,3]
    char b;     //長度1 < 8 按1對齊;偏移量爲4;存放位置區間[4]
    short c;    //長度2 < 8 按2對齊;偏移量由5提升到6;存放位置區間[6,7]
    //結構體內部最大元素爲int,由於偏移量爲8剛好是4的整數倍,所以從8開始存放接下來的struct FF
    struct FF
    {
        int a1;     //長度4 < 8 按4對齊;偏移量爲8;存放位置區間[8,11]
        char b1;    //長度1 < 8 按1對齊;偏移量爲12;存放位置區間[12]
        short c1;   //長度2 < 8 按2對齊;偏移量爲13,提升到2的倍數14;存放位置區間[14,15]
        char d1;    //長度1 < 8 按1對齊;偏移量爲16;存放位置區間[16]
    };
    //整體對齊係數 = min((max(int,short,char), 8) = 4,將內存大小由17補齊到4的整數倍20
    char d;         //長度1 < 8 按1對齊;偏移量爲21;存放位置區間[21]
    //整體對齊係數 = min((max(int,short,char), 8) = 4,將內存大小由21補齊到4的整數倍24
};

例五:再來一個嵌套結構體的計算
整體計算過程如下
struct B {
    char e[2];      //長度1 < 8 按2對齊;偏移量爲0;存放位置區間[0,1]
    short h;        //長度2 < 8 按2對齊;偏移量爲2;存放位置區間[2,3]
    //結構體內部最大元素爲double,偏移量爲4,提升到8,所以從8開始存放接下來的struct A
    struct A {
        int a;      //長度4 < 8 按4對齊;偏移量爲8;存放位置區間[8,11]
        double b;   //長度8 = 8 按8對齊;偏移量爲12,提升到16;存放位置區間16,23]
        float c;    //長度4 < 8,按4對齊;偏移量爲24,存放位置區間[24,27]
    };
    //整體對齊係數 = min((max(int,double,float), 8) = 8,將內存大小由28補齊到8的整數倍32
};

小結:當#pragma pack的n值等於或超過所有數據成員長度的時候,這個n值的大小將不產生任何效果。
會了關於結構體內存大小的計算,可是爲什麼系統要對於結構體數據進行內存對齊呢,很明顯所佔用的空間大小要更多。原因可歸納如下:
1、平臺原因(移植原因):不是所有的硬件平臺都能訪問任意地址上的任意數據的;某些硬件平臺只能在某些地址處取某些特定類型的數據,否則拋出硬件異常。
2、性能原因:數據結構(尤其是棧)應該儘可能地在自然邊界上對齊。原因在於,爲了訪問未對齊的內存,處理器需要作兩次內存訪問;而對齊的內存訪問僅需要一次訪問。
 


首先,cpu的訪問粒度爲4,也就是一次性可以讀取內存中的四個字節內容;當我們不採用內存對齊策略,如果需要訪問A中的b元素,cpu需要先取出0~3四個字節的內容,發現沒有讀取完,還需要再次讀取,一共需要進行兩次訪問內存的操作;而有了內存對齊,參考左圖,可一次性取出4~7四個字節的元素也即是b,這樣就只需要進行一次訪問內存的操作。所以操作系統這樣做的原因也就是所謂的拿空間換時間,提高效率。
建議:雖然操作系統會浪費空間來完成內存對齊,但是我們有了上面的知識可以通過按照數據類型來調整結構體內部的數據的先後順序來儘量減少內存的消耗;例如我們將下面結構體A中的順序調整爲B,sizeof(A)的結果爲12,而sizeof(B)的結果就是8:
struct A
{
    char a;
    int b;
    char c;
};

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

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