字節(jié)那些事兒
7、 如何控制字節(jié)對(duì)齊
本文引用地址:http://www.ex-cimer.com/article/201607/294782.htm控制程序的字節(jié)對(duì)齊行為是一個(gè)與編譯器相關(guān)的工作。以下編譯指示( directive )被許多編譯器認(rèn)可:
#pragma pack(n)
#pragma pack()
任何處于這兩個(gè)編譯指示語句之間的數(shù)據(jù)結(jié)構(gòu),將采用 n 字節(jié)的數(shù)據(jù)對(duì)齊方式。 n 是一個(gè)可以指定的數(shù)字,取值范圍請(qǐng)參閱所使用編譯器的文檔,通常都會(huì)取值為 2 的冪?,F(xiàn)代編譯器在對(duì)程序進(jìn)行編譯時(shí),處于效率方面的考慮,會(huì)對(duì)數(shù)據(jù)結(jié)構(gòu)的內(nèi)存布局使用一個(gè)默認(rèn)的字節(jié)對(duì)齊值,這個(gè)值一般都可以在命令行上顯式指定。如果要在一個(gè)頭文件 / 源文件中對(duì)特定的部分指定對(duì)齊屬性,則需要上述的編譯指示。結(jié)束指示的寫法在某些編譯器或者平臺(tái)下需要寫成:
#pragma pack(pop)
我們用一個(gè)例子來看一下這兩個(gè)指示的實(shí)際效用,看它究竟是如何影響數(shù)據(jù)的內(nèi)存排列的。假定我們有如下的數(shù)據(jù)結(jié)構(gòu)定義:
struct S1
{
int i;
char c;
short s;
};
struct S2
{
char c;
int i;
short s;
};
這兩個(gè)結(jié)構(gòu)的成員看起來是一樣的,只不過換了一下順序而已。我們使用 sizeof() 操作符來測(cè)量各自占用多少字節(jié)(除非特別指出,均在 32 位平臺(tái)上,并認(rèn)為 int 占用 4 字節(jié), char 占用 1 字節(jié), short 占用 2 字節(jié))。答案似乎不可思議, sizeof(S1) 的結(jié)果是 8 ,而 sizeof(S2) 卻是 12 。差異是怎么來的呢?原因就在于編譯器缺省的字節(jié)對(duì)齊設(shè)定在發(fā)生作用。
這里需要引入以下概念和規(guī)則:
概念及規(guī)則一,原生數(shù)據(jù)類型自身對(duì)齊值。原生數(shù)據(jù)類型即是 C/C++ 直接支持的數(shù)據(jù)類型,也可以稱為內(nèi)建(built in )數(shù)據(jù)類型。它們的自身對(duì)齊值分別為: char 為 1 , short int 為 2 , int 、 float 、 double 等為 4 ,不受符號(hào)位(即正負(fù))的影響。
概念及規(guī)則二,用戶數(shù)據(jù)類型自身對(duì)齊值。用戶數(shù)據(jù)類型即由程序員定義的類、結(jié)構(gòu)、聯(lián)合等,也叫抽象數(shù)據(jù)類型( ADT )。它們的自身對(duì)齊值等同于為其成員的對(duì)齊值中的最大值。
概念及規(guī)則三,用戶指定對(duì)齊值。程序員在編譯器命令行上的指定值,或者在 pragma pack 編譯指示中指定的值,對(duì)最終數(shù)據(jù)的影響取就近原則(顯然 pragma pack 指示會(huì)覆蓋命令行的指定)。
概念及規(guī)則四,有效對(duì)齊值。取數(shù)據(jù)類型的自身對(duì)齊值與用戶指定對(duì)齊值中的較小值。此值一旦決出,則會(huì)影響到數(shù)據(jù)在內(nèi)存中的布局。一個(gè)有效對(duì)齊值為 n ,表示以下事實(shí):相關(guān)數(shù)據(jù)在內(nèi)存中存放時(shí),其起始地址的值必須可以被 n 整除 。
根據(jù)以上四條,可以很圓滿地解釋 S1 和 S2 的大小不同這一現(xiàn)狀。由于沒有使用 pragma pack 指示,那么編譯器(在我的測(cè)試環(huán)境下)會(huì)采用缺省的對(duì)齊值 4 。假設(shè) S1 或者 S2 的實(shí)例將從地址 0x0000 處開始。
在 S1 中,第一個(gè)成員 i 的自身對(duì)齊值為 4 ,指定對(duì)齊值(盡管是缺省的)也是 4 ,同時(shí) 0x0000 這一地址符合被 4 整除的要求,因此, i 將占據(jù) 0x0000 到 0x0003 的四個(gè)字節(jié),下一個(gè)可用地址值為 0x0004 ;接下來的成員c 的數(shù)據(jù)類型為 char ,自身對(duì)齊值為 1 ,指定對(duì)齊值為 4 ,取較小者仍然是 1 , 0x0004 符合被 1 整除的要求,因此 c 將占據(jù) 0x0004 處的一個(gè)字節(jié),下一個(gè)可用地址值為 0x0005 ;最后的一個(gè)成員 s 數(shù)據(jù)類型為 short ,自身對(duì)齊值為 2 ,指定對(duì)齊值為 4 ,有效對(duì)齊值取 2 ,但是地址 0x0005 不能符合被 2 整除的要求,因此編譯器作相應(yīng)調(diào)整,向后移動(dòng)到最近的滿足要求的地址處,即 0x0006 , s 將占用 0x0006 和 0x0007 處的兩個(gè)字節(jié),由此導(dǎo)致S1 的大小為 8 。
在地址 0x0005 處的一個(gè)字節(jié),習(xí)慣上稱之為填充數(shù)據(jù)( padding )。
同理可以輕易推出 S2 結(jié)構(gòu)的大小確實(shí)是 12 。是這樣嗎?不是的。實(shí)際動(dòng)手的結(jié)果應(yīng)該是 10 。那么 12 應(yīng)該作何解釋?
我們來設(shè)想一個(gè)場(chǎng)景,程序員用 new 或者 malloc 分配一個(gè) S2 的數(shù)組。不用多,假定有兩個(gè)元素,而地址0x0000 處正好有空閑的內(nèi)存可以滿足這一內(nèi)存分配請(qǐng)求。我們都知道,在 C/C++ 語言中,數(shù)組的元素是緊鄰排放的。也就是說,后一個(gè)元素的起始地址應(yīng)該正好等于前一個(gè)元素的起始地址,并加上元素的大小。我們來檢視一下S2 的情況,它的元素大小為 10 ,它的有效對(duì)齊值是 4 (請(qǐng)參閱概念及規(guī)則二),這表示任何一個(gè) S2 結(jié)構(gòu)的起始地址都應(yīng)該位于 4 的整數(shù)倍處?,F(xiàn)實(shí)的情況是,第一個(gè)元素的起始地址是 0x0000 ,第二個(gè)元素的起始地址變成了0x000A ,而后者的數(shù)值不能滿足被 4 整除的要求。正是為了解決這一情況,編譯器為 S2 結(jié)構(gòu)在結(jié)尾處也增加了兩個(gè)字節(jié)的填充,從而滿足各個(gè)條件的限定。
pragma pack 指示非常有效,使用也比較普遍,但是對(duì)于 ARM 平臺(tái),它有一些力所不及的地方,我們?cè)賮砜匆粋€(gè)例子。仍然用 S2 ,這一次,我們強(qiáng)制把它的字節(jié)對(duì)齊設(shè)定為 1 ,并同時(shí)定義了 S2 的一個(gè)全局變量 s2 。也即:
#pragma pack(1)
struct S2
{
char c;
int i;
short s;
} s2;
#pragma pop()
然后,在某處具有如下的數(shù)據(jù)訪問:
int i = s2.i;
這條看上去稀松平常的語句很可能不能如所希望的那樣執(zhí)行。因?yàn)閷?duì)于 i 的訪問其前提應(yīng)該是 i 的起始地址是 4的倍數(shù)(注意,這個(gè)不是對(duì)齊規(guī)則的約束結(jié)果,而是 ARM CPU 的數(shù)據(jù)訪問規(guī)則的約束結(jié)果),但強(qiáng)行指定的 1 字節(jié)對(duì)齊則導(dǎo)致 i 的起始地址是一個(gè)奇數(shù)。
RVCT 編譯器為此做了特別的努力,引入了 __packed 關(guān)鍵字。此關(guān)鍵字應(yīng)用到用戶定義數(shù)據(jù)結(jié)構(gòu)上會(huì)導(dǎo)致該結(jié)構(gòu)的內(nèi)存布局取得與 pragma pack(1) 等同的效果,但是,更進(jìn)一步地,編譯器會(huì)把對(duì)該結(jié)構(gòu)中成員的訪問作適當(dāng)?shù)奶幚?,發(fā)現(xiàn)不對(duì)齊的訪問則會(huì)翻譯為調(diào)用適當(dāng)?shù)谋WC數(shù)據(jù)正確性的函數(shù)。此關(guān)鍵字也可以應(yīng)用到指針上,以保證經(jīng)由指針對(duì)目標(biāo)對(duì)象的訪問也采用保守方式??梢灶A(yù)料到的是,此關(guān)鍵字的使用會(huì)降低代碼執(zhí)行的效率,所以需要慎用,一個(gè)很典型的使用場(chǎng)景是移植其他平臺(tái)的代碼時(shí)。以下是一些使用了此關(guān)鍵字的定義示例:
typedef __packed struct
{
char x; // 所有成員都會(huì)被 __packed 修飾
int y;
} X; // 5 字節(jié)的結(jié)構(gòu),自身對(duì)齊值 = 1
int f(X* p)
{
return p->y; // 執(zhí)行一個(gè)非對(duì)齊的讀取操作
}
typedef struct
{
short x;
char y;
__packed int z; // 僅 __pack 本成員,此用法僅適用于整型
char a;
} Y; // 8 字節(jié)結(jié)構(gòu),自身對(duì)齊值 = 2(請(qǐng)思考原因)
int g(Y* p)
{
return p->z + p->x; // 僅對(duì) z 執(zhí)行非對(duì)齊讀取操作
}
需要注意的是, GCCE 編譯器沒有實(shí)現(xiàn)類似的努力,它有一個(gè)和對(duì)齊有關(guān)的關(guān)鍵字: __attribute__ (packed)),該關(guān)鍵字的功效與 pragma pack(1) 類似。
8、 思考 / 練習(xí)題
a) 位( bit )在字節(jié)中的排列,應(yīng)該也有類似字節(jié)序那樣的問題,為什么沒有?
b) 自己寫幾個(gè)結(jié)構(gòu),根據(jù)規(guī)則推斷其大小,然后寫代碼驗(yàn)證
c) 請(qǐng)查閱 RVCT 的相關(guān)文檔,學(xué)習(xí) __align 關(guān)鍵字的含義和用法
d) 了解微軟公司針對(duì) Windows Mobile 平臺(tái)的編譯器是否也具有幫助程序員自動(dòng)解決對(duì)其訪問的機(jī)制
9、 參考資料
a) 《編程卓越之道》,第一卷
b) 《 RealView Compilation Tools - Compiler and Libraries Guide 》
c) ARM Information Center
d) http://blog.csdn.net/xhfwr/archive/2006/07/23/963793.aspx
評(píng)論