一、什么是字節(jié)對齊
一個基本類型的變量在內(nèi)存中占用n個字節(jié),則該變量的起始地址必須能夠被n整除,即: 存放起始地址 % n = 0,那么,就成該變量是字節(jié)對齊的;對于結(jié)構(gòu)體、聯(lián)合體而言,這個n取其所有基本類型的成員中占用空間字節(jié)數(shù)最大的那個;
內(nèi)存空間是以字節(jié)為基本單位進行劃分的,從理論上講,似乎對任何類型的變量的訪問都可以從任何地址處開始,但實際情況是在訪問特定類型變量的時候經(jīng)常是從特定的內(nèi)存地址處開始訪問,這就需要各種類型的數(shù)據(jù)只能按照一定的規(guī)則在空間上排列,而不是順序的一個接一個地排放;究其原因,是為了使不同架構(gòu)的CPU可以提高訪問內(nèi)存的速度,就規(guī)定了對于特定類型的數(shù)據(jù)只能從特定的內(nèi)存位置處開始訪問;所以,各種類型的數(shù)據(jù)只能按照相應(yīng)的規(guī)則在內(nèi)存空間上排放,而不能順序地、連續(xù)地、一個一個地排放;這就是內(nèi)存對齊;
二、為什么需要字節(jié)對齊
由于各種硬件平臺對存儲空間的處理上有很大的不同;一些平臺對某些特定類型的數(shù)據(jù)只能從某個特定內(nèi)存地址處開始訪問;比如:有些架構(gòu)的CPU在訪問一個沒有進行對齊的變量的時候會發(fā)生錯誤,那么在這種架構(gòu)下編程時就必須保證字節(jié)對齊;其它平臺可能沒有這種情況,但最常見的是,如果不按照適合其平臺要求對數(shù)據(jù)進行對齊,會在存取效率上帶來損失;比如,有些平臺每次讀取數(shù)據(jù)都是從偶地址處開始,如果一個int(假設(shè)為32位系統(tǒng))型數(shù)據(jù)從偶地址處開始存放,那么只需要一個讀指令周期就可以完全讀出這個32bit的int型數(shù)據(jù),相反,如果這個32bit的int型數(shù)據(jù)是從奇地址處開始存放,那么就需要兩個讀指令周期才能完全讀出這個32bit的int數(shù)據(jù),并且還需要對這兩次讀出的結(jié)果的高低字節(jié)進行重新拼湊才能得到正確的32bit數(shù)據(jù);這個時候,CPU的讀取效率明顯下降;
三、字節(jié)對齊規(guī)則
預(yù)處理指令#pragma pack(align_value)用于指定對齊值,而預(yù)處理指令#pragma pack()用于取消上次設(shè)定的對齊值,恢復(fù)默認(rèn)對齊值;
字節(jié)對齊是針對基本類型變量的;基本類型變量有:char、unsigned char、short、unsigned short、int、unsigned int、long、unsigned long、long long、unsigned long long、float、double,等等;所以,對于結(jié)構(gòu)體的對齊也只能按照其成員變量中的基本類型來對齊了;
有四個概念需要理解:
A、數(shù)據(jù)類型自身的對齊值:
是指對該數(shù)據(jù)類型使用sizeof()操作符進行操作所得到的大小(單位,字節(jié));比如,對于[unsigned] char類型的數(shù)據(jù),其自身對齊值為1字節(jié);對于[unsigned] short類型的數(shù)據(jù),其自身對齊值是2字節(jié);對于[unsigned] int、[unsigned] long、[unsigned] long long、float、double等數(shù)據(jù)類型,其自身對齊值是4字節(jié);
B、結(jié)構(gòu)體、聯(lián)合體、類的自身對齊值:
是指其所有基本類型的成員中,自身對齊值最大的那個值;如果這些復(fù)合類型中有嵌套類型或復(fù)合類型的變量,則需要把這些嵌套的類型或復(fù)合類型的變量拆解成基本類型的成員之后再對齊;
C、指定對齊值:
是指使用預(yù)處理指令#pragma pack(align_value)指定的對齊值align_value;
D、數(shù)據(jù)成員、結(jié)構(gòu)體和類的有效對齊值:
是指其自身對齊值和指定對齊值中較小的那個值;
其中,有效對齊值是最終用來決定數(shù)據(jù)存放地址方式的值,最重要;設(shè)定有效對齊值為N,就表示"對齊在N字節(jié)上",也就是說,該數(shù)據(jù)的"存放起始地址%N=0";
因此,每個類型的數(shù)據(jù)的有效對齊值就是其自身對齊值(通常是這個類型的大小)和指定對齊值(不指定則取默認(rèn)值)中較小的那個值,并且結(jié)構(gòu)體自身對齊值是其所有成員中自身對齊值最大的那個值;
字節(jié)對齊的細(xì)節(jié)與編譯器的實現(xiàn)有關(guān),但一般來說,結(jié)構(gòu)體需要滿足以下幾個準(zhǔn)則:
1).從結(jié)構(gòu)體外部來看,結(jié)構(gòu)體變量的首地址能夠被其最寬基本成員的大小整除;從結(jié)構(gòu)體內(nèi)部來看,它的第一個數(shù)據(jù)成員的地址相對于整個結(jié)構(gòu)體首地址的偏移量為0,也就是說,結(jié)構(gòu)體的第一個數(shù)據(jù)成員存放在偏移量為0的地方;
2).結(jié)構(gòu)體中的每個數(shù)據(jù)成員的有效對齊值都取其自身對齊值和指定對齊值中的較小的那個對齊值;或者說是,結(jié)構(gòu)體中的每個數(shù)據(jù)成員相對于結(jié)構(gòu)體首地址的偏移量都是該數(shù)據(jù)成員大小和指定對齊值中較小的那個值(或有效對齊值)的整數(shù)倍,如有需要,編譯器會在數(shù)據(jù)成員之間加上填充字節(jié);
3).如果結(jié)構(gòu)體中還有嵌套的結(jié)構(gòu)體或結(jié)構(gòu)體變量,那么就要把這些嵌套進去的結(jié)構(gòu)體或結(jié)構(gòu)體變量拆成基本類型成員,并取其最長的基本類型成員的對齊方式;
4).結(jié)構(gòu)體整體的有效對齊值必須為其最寬基本類型成員大小的整數(shù)倍;或者說是,結(jié)構(gòu)體整體的大小為結(jié)構(gòu)體中最寬基本類型成員大小的整數(shù)倍,如有需要,編譯器會在最末一個成員之后加上填充字節(jié);換句話說是,結(jié)構(gòu)體整體的有效對齊值按照結(jié)構(gòu)體中最寬基本類型成員的大小和指定對齊值中較小的那個值進行;
注意:如果指定對齊值大于自身對齊值,則指定對齊值無效;
例1:不帶嵌套的
#pragma pack(4) //指定按照4字節(jié)對齊
struct TestA
{
int a; //第一個成員,自身長4,#pragma pack(4),取較小值,按照4字節(jié)對齊,放在[0,3]偏移的位置;
char b; //第二個成員,自身長1,#pragma pack(4),取較小值,按照1字節(jié)對齊,放在[4]偏移的位置;
short c; //第三個成員,自身長1,#pragma pack(4),取較小值,按照2字節(jié)對齊,偏移量必須是2的整數(shù)倍,故,存放在[6,7]偏移的位置;
char d; //第四個成員,自身長1,#pragma pack(4),取較小值,按照1字節(jié)對齊,放在[8]偏移的位置;
};
#pragma pack() //取消4字節(jié)對齊,恢復(fù)默認(rèn)對齊值;
因此,整個結(jié)構(gòu)體占用的有效字節(jié)為9個字節(jié);由于結(jié)構(gòu)體整體的對齊值和大小是其最寬基本類型成員大小的整數(shù)倍,即:按照最寬基本類型成員大小和指定對齊值中較小的值對齊的;因為結(jié)構(gòu)體最寬基本類型成員的大小是4字節(jié),其有效對齊值也是4字節(jié),而9字節(jié)按照4字節(jié)圓整的結(jié)果是12字節(jié),所以,sizeof(TestA)=12;
#pragma pack(2) //指定按照2字節(jié)對齊
struct TestA
{
int a; //第一個成員,自身長4,#pragma pack(4),取較小值,按照2字節(jié)對齊,放在[0,3]偏移的位置;
char b; //第二個成員,自身長1,#pragma pack(2),取較小值,按照1字節(jié)對齊,放在[4]偏移的位置;
short c; //第三個成員,自身長1,#pragma pack(2),取較小值,按照2字節(jié)對齊,偏移量必須是2的整數(shù)倍,故,存放在[6,7]偏移的位置;
char d; //第四個成員,自身長1,#pragma pack(2),取較小值,按照1字節(jié)對齊,放在[8]偏移的位置;
};
#pragma pack() //取消4字節(jié)對齊,恢復(fù)默認(rèn)對齊值;
可以看出,只是改變了一下結(jié)構(gòu)體之間的對齊方式,從4字節(jié)對齊改為2字節(jié)對齊;結(jié)果就不一樣了;
整個結(jié)構(gòu)體所占用的有效字節(jié)數(shù)仍然是9字節(jié),但是結(jié)構(gòu)體整體的大小就變了,按照最寬基本類型成員大小和指定對齊值中較小的值對齊;結(jié)構(gòu)體中最寬基本類型成員的大小事4字節(jié),而指定對齊值是2字節(jié)對齊,取最小值2,所以,最后,整個結(jié)構(gòu)體的大小就是9字節(jié)按照2字節(jié)圓整(取2的整數(shù)倍),于是,sizeof(TestA)=10;
例2:帶嵌套的
#pragma pack(2)
struct A
{
char c; //第一個成員,自身長1,#pragma pack(2),取較小值,按照1字節(jié)對齊,放在[0]偏移的位置;
int i; //第二個成員,自身長4,#pragma pack(2),取較小值,按照2字節(jié)對齊,放在[2,5]偏移的位置;
};
struct B
{
char c1; //第一個成員,自身長1,#pragma pack(2),取較小值,按照1字節(jié)對齊,放在[0]偏移的位置;
A s; //第二個成員,自身長6,#pragma pack(2),取較小值,按照2字節(jié)對齊,放在[2,7]偏移的位置;
char c2; //第三個成員,自身長1,#pragma pack(2),取較小值,按照1字節(jié)對齊,存放在[8]偏移的位置;
};
#pragma pack()
結(jié)構(gòu)體A占用的有效字節(jié)數(shù)是6字節(jié);結(jié)構(gòu)體A整體的大小要取其最寬基本類型成員大小和指定對齊值中較小的那個值的整數(shù)倍,最寬基本類型成員大小為4字節(jié),指定對齊值為2字節(jié),所以,6取較小的值2字節(jié)的整數(shù)倍為6字節(jié);最終,結(jié)構(gòu)體A的大小為:sizeof(A)=6;
結(jié)構(gòu)體B占用的有效字節(jié)數(shù)是9字節(jié);結(jié)構(gòu)體B整體的大小要取其最寬基本類型成員大小和指定對齊值中較小的那個值的整數(shù)倍,最寬基本類型成員大小為6字節(jié),指定對齊值為2字節(jié),所以,9取較小的值2字節(jié)的整數(shù)倍為10字節(jié);最終,結(jié)構(gòu)體B的大小為:sizeof(B)=10;
四、總結(jié)
設(shè)置對齊方式有兩種方法;
第一種方法:
#pragma pack(n),指定按照n字節(jié)對齊;
#pragma pack(),取消自定義的對齊值;
第二種方法:
__attribute__((aligned(n))):讓所作用的結(jié)構(gòu)體成員對齊在n字節(jié)自然邊界上;如果結(jié)構(gòu)體中有成員的長度大于n,則按照最大成員的長度來對齊;即:按照機器能允許該類的的最大長度來對齊;這個恰好與#pragma pack(n)指令的相反;
__attribute__((packed)):取消結(jié)構(gòu)體在編譯過程中的優(yōu)化對齊,按照實際占用字節(jié)數(shù)進行對齊,等價于指令#pragma pack(1),即,按照1字節(jié)對齊;
其中,n=1,2,4,8,16,......等;
第一種方法比較常見;