深入研究字節對齊問題

1.       對齊的原因與作用

1.1.   對齊的原因

各種硬件平臺對存儲空間的處理上有很大的不同。一些平臺對某些特定類型的數據只能從某些特定地址開始存取。比如有些架構的 CPU 在訪問一個沒有進行對齊的變量的時候會發生錯誤,那麼在這種架構下編程必須保證字節對齊。

1.2.   對齊的作用

最常見的情況是,如果不按照適合其平臺要求對數據存放進行對齊,會在存取效率上帶來損失。比如有些平臺每次讀都是從偶地址開始,如果一個 int 型(假設爲 32 位系統)存放在偶地址開始的地方,那麼一個讀週期就可以讀 32bit ,而如果存放在奇地址開始的地方,就需 2 個讀週期,並對兩次讀出的結果的高低字節進行拼湊才能得到 32bit 數據。顯然在讀取效率上下降很多。

如果都按照該 cpu 對齊格式對齊了的話,可以大大減少 cpu 讀週期的數目,明顯提高了運算的效率。

x86 上,類似的操作只會影響效率,因爲 x86 支持自動對齊。但是在 MIPS 或者 sparc 上,可能就是一個 error ,因爲它們要求必須字節對齊。

 

2.       對齊的實現

通常,我們寫程序的時候,不需要考慮對齊問題。編譯器會替我們選擇適合目標平臺的對齊策略。當然,我們也可以通知給編譯器傳遞預編譯指令而改變對指定數據的對齊方法。

但是,如果我們從不關心這個問題,在有些情況下可能會出錯。比如第三方庫、 IPC 之間發送內存數據、二進制網絡協議等等,有可能使用不同的編譯器並設置不同的字節對齊方式,因此就有可能帶來一些莫名其妙的錯誤,對於相同的結構體或類對象 sizeof 出來的大小可能差別很大。

 

3.       字節對齊對程序的影響

先讓我們看幾個例子吧 (32bit,x86 環境 ,gcc 編譯器 ):
設結構體如下定義:

struct A
{
    int a;

char b;
    short c;
};
struct B
{
    char b;
    int a;
    short c;
};
現在已知 32 位機器上各種數據類型的長度如下
:
char:1(
有符號無符號同
)    
short:2(
有符號無符號同
)    
int:4(
有符號無符號同
)    
long:4(
有符號無符號同
)    
float:4    

double:8
那麼上面兩個結構大小如何呢
?
結果是
:
sizeof(strcut A)
值爲
8
sizeof(struct B)
的值卻是 12

結構體 A 中包含了 4 字節長度的 int 一個, 1 字節長度的 char 一個和 2 字節長度的 short 型數據一個 ,B 也一樣 ; 按理說 A,B 大小應該都是 7 字節。
之所以出現上面的結果是因爲編譯器要對數據成員在空間上進行對齊。上面是按照編譯器的默認設置進行對齊的結果 , 默認對齊設置爲 4 字節。那麼我們是不是可以改變編譯器的這種默認對齊設置呢 , 當然可以 . 例如 :
#pragma pack (2) /*
指定按 2 字節對齊
*/
struct C
{
    char b;
    int a;
    short c;
};
#pragma pack () /*
取消指定對齊,恢復缺省對齊
*/
sizeof(struct C)
值是 8

修改對齊值爲 1
#pragma pack (1) /*
指定按 1 字節對齊 */
struct D
{
    char b;

    int a;
    short c;
};
#pragma pack () /*
取消指定對齊,恢復缺省對齊
*/
sizeof(struct D)
值爲 7

後面我們再講解 #pragma pack() 的作用。

4.       設置默認對齊值

4.1.        vc 設置方法

1.        VC IDE 中,可以這樣修改: [Project]|[Settings],c/c++ 選項卡 Category Code Generation 選項的 Struct Member Alignment 中修改,默認是 8 字節。

2.        在編碼時,可以這樣動態修改: #pragma pack . 注意 : pragma 而不是 progma

4.2.        gcc 設置方面

在代碼中添加: #pragma pack ( 對齊字節值 )

 

5.       字節對齊規則

5.1.        基本概念


1.
數據類型自身的對齊值:

對於 char 型數據,其自身對齊值爲 1 ,對於 short 型爲 2 ,對於 int,float 類型,其自身對齊值爲 4 double 8 ,單位字節。
2.
結構體或者類的自身對齊值: 其成員中自身對齊值最大的那個值。
3.
指定對齊值 #pragma pack (value) 時的指定對齊值 value
4.
數據成員、結構體和類的有效對齊值: 自身對齊值和指定對齊值中小的那個值。
   

5.2.        對齊算法

有了這些概念,我們就可以很方便的來討論具體數據結構的成員和其自身的對齊方式。有效對齊值 N 是最終用來決定數據存放地址方式的值,最重要。有效對齊 N ,就是表示 對齊在 N ,也就是說該數據的 " 存放起始地址 %N=0 " 。而數據結構中的數據變量都是按定義的先後順序來排放的。第一個數據變量的起始地址就是數據結構的起始地址。結構體的成員變量要對齊排放,結構體本身也要根據自身的有效對齊值圓整 ( 就是結構體成員變量佔用總長度需要是對結構體有效對齊值的整數倍,結合下面例子理解 )

示例分析

示例一:
struct B
{
    char b;
    int a;
    short c;
};
假設 B 從地址空間 0x0000 開始排放,默認字節對齊值爲 4 。第一個成員變量 b 的自身對齊值是 1 ,比指定或者默認指定對齊值 4 小,所以其有效對齊值爲 1 ,所以其存放地址 0x0000 符合 0x0000%1=0 。第二個成員變量 a ,其自身對齊值爲 4 ,所以有效對齊值也爲 4 所以只能存放在起始地址爲 0x0004 0x0007 這四個連續的字節空間中,複覈 0x0004%4=0, 且緊靠第一個變量。第三個變量 c, 自身對齊值爲 2 ,所以有效對齊值也是 2 ,可以存放在 0x0008 0x0009 這兩個字節空間中,符合 0x0008%2=0 。所以從 0x0000 0x0009 存放的 都是 B 內容。再看數據結構 B 的自身對齊值爲其變量中最大對齊值 ( 這裏是 b )所以就是 4 ,所以結構體的有效對齊值也是 4 。根據結構體圓整的要求, 0x0009 0x0000=10 字節,( 10 2 )% 4 0 。所以 0x0000A 0x000B 也爲結構體 B 所佔用。故 B 0x0000 0x000B 共有 12 個字節 ,sizeof(struct B)=12

其實如果就這一個就來說它已將滿足字節對齊了 , 因爲它的起始地址是 0, 因此肯定是對齊的 , 之所以在後面補充 2 個字節 , 是因爲編譯器爲了實現結構數組的存取效率。試想,如果我們定義了一個結構 B 的數組 , 麼第一個結構起始地址是 0 沒有問題 , 但是第二個結構呢 ? 按照數組的定義 , 數組中所有元素都是緊挨着的 , 如果我們不把結構的大小補充爲 4 的整數倍 , 那麼下一 個結構的起始地址將是 0x0000A, 這顯然不能滿足結構的地址對齊了 , 因此我們要把結構補充成有效對齊大小的整數倍。其實諸如 : 對於 char 型數據,其自身對齊值爲 1 ,對於 short 型爲 2 ,對於 int,float 類型,其自身對齊值爲 4 ,這些已有類型的自身對齊值也是基於數組考慮的,只是因爲這些類型的長度已知了 , 所以他們的自身對齊值也就已知了。
再分析一個:
#pragma pack (2) /*
指定按 2 字節對齊 */
struct C
{
    char b;
    int a;
    short c;
};
#pragma pack () /*
取消指定對齊,恢復缺省對齊
*/
第一個變量 b 的自身對齊值爲 1 ,指定對齊值爲 2 ,所以,其有效對齊值爲 1 ,假設 C 0x0000 開始,那麼 b 存放在 0x0000 ,符合 0x0000%1= 0; 第二個變量,自身對齊值爲 4 ,指定對齊值爲 2 ,所以有效對齊值爲 2 ,所以順序存放在 0x0002 0x0003 0x0004 0x0005 四個連續 字節中,符合 0x0002%2=0 。第三個變量 c 的自身對齊值爲 2 ,所以有效對齊值爲 2 ,順序存放在 0x0006 0x0007 中,符合 0x0006%2=0 。所以從 0x0000 0x00007 共八字節存放的是 C 的變量。又 C 的自身對齊值爲 4 ,所以 C 的有效對齊值爲 2 。又 8%2=0,C 只佔用 0x0000 0x0007 的八個字節。所以 sizeof(struct C)=8

5.3.        其他細節

1 、數組的自身對齊值爲元素對齊值。

2 、嵌套結構體對齊值爲打散後內部最大的對齊值。

3 x86 中有個設置是否檢查字節對齊的選項,但是windows 都沒有設置這個選項,缺省爲0 (不檢查,自動對齊)。

4 、單個簡單數據類型,如intlongdouble 等字節對齊大小不受編譯器影響。

5 、當數組作爲函數的參數進行傳遞時,該數組自動退化爲同類型的指針,sizeof 大小爲4

 

 

6.       在程序中處理字節對齊問題

1 )代碼中關於對齊的隱患,很多是隱式的。比如在強制類型轉換的時候。

例如:
unsigned int i = 0x12345678;
unsigned char *p=NULL;
unsigned short *p1=NULL;

p=&i;
*p=0x00;
p1=(unsigned short *)(p+1);
*p1=0x0000;
最後兩句代碼,從奇數邊界去訪問 unsigned short 型變量,顯然不符合對齊的規定。

2 )如果在編程的時候要考慮節約空間的話 , 那麼我們只需要假定結構的首地址是 0, 然後各個變量按照上面的原則進行排列即可 , 基本的原則就是把結構中的變量按照類型大小從大到小聲明 , 儘量減少中間的填補空間。還有一種就是爲了以空間換取時間的效率 , 我們顯示的進行填補空間進行對齊 , 比如 : 有一種使用空間換時間做 法是顯式的插入 reserved 成員:
         struct A{
           char a;
           char reserved[3];//
使用空間換時間
           int b;
}
reserved
成員對我們的程序沒有什麼意義 , 它只是起到填補空間以達到字節對齊的目的 , 當然即使不加這個成員通常編譯器也會給我們自動填補對齊 , 我們自己加上它只是起到顯式的提醒作用。

 

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