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