最近在調(diào)試中遇到點內(nèi)存對齊的問題,別人問我是怎么回事,我趕緊偷偷查了一下,記錄下來。
不論是C、C++對于內(nèi)存對齊的問題在原理上是一致的,對齊的原因和表現(xiàn),簡單總結(jié)一下,以便朋友們共享。
一、內(nèi)存對齊的原因
大部分的參考資料都是如是說的:
1、平臺原因(移植原因):不是所有的硬件平臺都能訪問任意地址上的任意數(shù)據(jù)的;某些硬件平臺只能在某些地址處取某些特定類型的數(shù)據(jù),否則拋出硬件異常。
2、性能原因:數(shù)據(jù)結(jié)構(gòu)(尤其是棧)應(yīng)該盡可能地在自然邊界上對齊。原因在于,為了訪問未對齊的內(nèi)存,處理器需要作兩次內(nèi)存訪問;而對齊的內(nèi)存訪問僅需要一次訪問。
也有的朋友說,內(nèi)存對齊出于對讀取的效率和數(shù)據(jù)的安全的考慮,我覺得也有一定的道理。
二、對齊規(guī)則
每個特定平臺上的編譯器都有自己的默認“對齊系數(shù)”(也叫對齊模數(shù))。比如32位windows平臺下,VC默認是按照8bytes對齊的(VC->Project->settings->c/c++->Code Generation中的truct member alignment 值默認是8),程序員可以通過預(yù)編譯命令#pragma pack(n),n=1,2,4,8,16來改變這一系數(shù),其中的n就是你要指定的“對齊系數(shù)”。
在嵌入式環(huán)境下,對齊往往與數(shù)據(jù)類型有關(guān),特別是C編譯器對缺省的結(jié)構(gòu)成員自然對屆條件為“N字節(jié)對 齊”,N即該成員數(shù)據(jù)類型的長度。如int型成員的自然對界條件為4字節(jié)對齊,而double類型的結(jié)構(gòu)成員的自然對界條件為8字節(jié)對齊。若該成員的起始 偏移不位于該成員的“默認自然對界條件”上,則在前一個節(jié)面后面添加適當個數(shù)的空字節(jié)。C編譯器缺省的結(jié)構(gòu)整體的自然對界條件為:該結(jié)構(gòu)所有成員中要求的 最大自然對界條件。若結(jié)構(gòu)體各成員長度之和不為“結(jié)構(gòu)整體自然對界條件的整數(shù)倍,則在最后一個成員后填充空字節(jié)。
那么可以得到如下的小結(jié):
類型 對齊方式(變量存放的起始地址相對于結(jié)構(gòu)的起始地址的偏移量)
Char 偏移量必須為sizeof(char)即1的倍數(shù)
Short 偏移量必須為sizeof(short)即2的倍數(shù)
int 偏移量必須為sizeof(int)即4的倍數(shù)
float 偏移量必須為sizeof(float)即4的倍數(shù)
double 偏移量必須為sizeof(double)即8的倍數(shù)
各成員變量在存放的時候根據(jù)在結(jié)構(gòu)中出現(xiàn)的順序依次申請空間,同時按照上面的對齊方式調(diào)整位置,空缺的字節(jié)編譯器會自動填充。同時為了確保結(jié)構(gòu)的大小為結(jié) 構(gòu)的字節(jié)邊界數(shù)(即該結(jié)構(gòu)中占用最大空間的類型所占用的字節(jié)數(shù))的倍數(shù),所以在為最后一個成員變量申請空間后,還會根據(jù)需要自動填充空缺的字節(jié),也就是 說:結(jié)構(gòu)體的總大小為結(jié)構(gòu)體最寬基本類型成員大小的整數(shù)倍,如有需要編譯器會在最末一個成員之后加上填充字節(jié)。對于char數(shù)組,字節(jié)寬度仍然認為為1。
對于下述的一個結(jié)構(gòu)體,其對齊方式為:
struct Node1{
double m1;
char m2;
int m3;
};
對于第一個變量m1,sizeof(double)=8個字節(jié);接下來為第二個成員m2分配空間,這時下一個可以分配的地址對于結(jié)構(gòu)的起始地址的偏移量為8,是sizeof(char)的倍數(shù),所以把m2存放在偏移量為8的地方滿足對齊方式,該成員變量占用 sizeof(char)=1個字節(jié);接下來為第三個成員m3分配空間,這時下一個可以分配的地址對于結(jié)構(gòu)的起始地址的偏移量為9,不是sizeof (int)=4的倍數(shù),為了滿足對齊方式對偏移量的約束問題,自動填充3個字節(jié)(這三個字節(jié)沒有放什么東西),這時下一個可以分配的地址對于結(jié)構(gòu)的起始地址的偏移量為12,剛好是sizeof(int), 由于8+4+4 = 16恰好是結(jié)構(gòu)體中最大空間類型double(8)的倍數(shù),所以sizeof(Node1) =16.
typedef struct{
char a;
int b;
char c;
}Node2;
成員a占一個字節(jié),所以a放在了第1位的位置;由于第二個變量b占4個字節(jié),為保證起始位置是4(sizeof(b))的倍數(shù),所以需要在a后面填充3個 字節(jié),也就是b放在了從第5位到第8位的位置,然后就是c放在了9的位置,此時4+4+1=9。接下來考慮字節(jié)邊界數(shù),9并不是最大空間類型int(4) 的倍數(shù),應(yīng)該取大于9且是4的的最小整數(shù)12,所以sizeof(Node2) = 12.
typedef struct{
char a;
char b;
int c;
}Node3;
明顯地:sizeof(Node3) = 8
對于結(jié)構(gòu)體A中包含結(jié)構(gòu)體B的情況,將結(jié)構(gòu)體A中的結(jié)構(gòu)體成員B中的最寬的數(shù)據(jù)類型作為該結(jié)構(gòu)體成員B的數(shù)據(jù)寬度,同時結(jié)構(gòu)體成員B必須滿足上述對齊的規(guī)定。
要注意在VC中有一個對齊系數(shù)的概念,若設(shè)置了對齊系數(shù),那么上述描述的對齊方式,則不適合。
例如:
1字節(jié)對齊(#pragma pack(1))
輸出結(jié)果:sizeof(struct test_t) = 8 [兩個編譯器輸出一致]
分析過程:
成員數(shù)據(jù)對齊
#pragma pack(1)
struct test_t {
int a;
char b;
short c;
char d;
};
#pragma pack()
成員總大小=8;
2字節(jié)對齊(#pragma pack(2))
輸出結(jié)果:sizeof(struct test_t) = 10 [兩個編譯器輸出一致]
分析過程:
成員數(shù)據(jù)對齊
#pragma pack(2)
struct test_t {
int a;
char b;
short c;
char d;
};
#pragma pack()
成員總大小=9;
4字節(jié)對齊(#pragma pack(4))
輸出結(jié)果:sizeof(struct test_t) = 12 [兩個編譯器輸出一致]
分析過程:
1) 成員數(shù)據(jù)對齊
#pragma pack(4)
struct test_t { //按幾對齊, 偏移量為后邊第一個取模為零的。
int a;
char b;
short c;
char d;
};
#pragma pack()
成員總大小=9;
8字節(jié)對齊(#pragma pack(8))
輸出結(jié)果:sizeof(struct test_t) = 12 [兩個編譯器輸出一致]
分析過程:
成員數(shù)據(jù)對齊
#pragma pack(8)
struct test_t {
int a;
char b;
short c;
char d;
};
#pragma pack()
成員總大小=9;
16字節(jié)對齊(#pragma pack(16))
輸出結(jié)果:sizeof(struct test_t) = 12 [兩個編譯器輸出一致]
分析過程:
1) 成員數(shù)據(jù)對齊
#pragma pack(16)
struct test_t {
int a;
char b;
short c;
char d;
};
#pragma pack()
成員總大小=9;
至于8字節(jié)對齊和16字節(jié)對齊,我覺得這兩個例子取得不好,沒有太大的參考意義。
(x666f)