1. 程式人生 > 實用技巧 >位元組對齊

位元組對齊

轉載自:https://blog.csdn.net/q1449516487/article/details/100598325

概念

  在C語言中,結構是一種複合資料型別,其構成元素既可以是基本資料型別(如int、long、float等)的變數,也可以是一些複合資料型別(如陣列、結構、聯合等)的資料單元。在結構中,編譯器為結構的每個成員按其自然邊界(alignment)分配空間。各個成員按照它們被宣告的順序在記憶體中順序儲存,第一個成員的地址和整個結構的地址相同。

  為了使CPU能夠對變數進行快速的訪問,變數的起始地址應該具有某些特性,即所謂的”對齊”,比如4位元組的int型,其起始地址應該位於4位元組的邊界上,即起始地址能夠被4整除,也即對齊跟資料在記憶體中的位置有關。如果一個變數的記憶體地址正好位於它長度的整數倍,他就被稱做自然對齊。

  比如在32位cpu下,假設一個整型變數的地址為0x00000004(為4的倍數),那它就是自然對齊的,而如果其地址為0x00000002(非4的倍數)則是非對齊的。

  現代計算機中記憶體空間都是按照byte劃分的,從理論上講似乎對任何型別的變數的訪問可以從任何地址開始,但實際情況是在訪問特定型別變數的時候經常在特定的記憶體地址訪問,這就需要各種型別資料按照一定的規則在空間上排列,而不是順序的一個接一個的排放,這就是對齊。

為什麼要位元組對齊

  需要位元組對齊的根本原因在於CPU訪問資料的效率問題。假設上面整型變數的地址不是自然對齊,比如為0x00000002,則CPU如果取它的值的話需要訪問兩次記憶體,第一次取從0x00000002-0x00000003的一個short,第二次取從0x00000004-0x00000005的一個short然後組合得到所要的資料,如果變數在0x00000003地址上的話則要訪問三次記憶體,第一次為char,第二次為short,第三次為char,然後組合得到整型資料。

  而如果變數在自然對齊位置上,則只要一次就可以取出資料。一些系統對對齊要求非常嚴格,比如sparc系統,如果取未對齊的資料會發生錯誤,而在x86上就不會出現錯誤,只是效率下降。

  各個硬體平臺對儲存空間的處理上有很大的不同。一些平臺對某些特定型別的資料只能從某些特定地址開始存取。

  比如有些架構的CPU在訪問一個沒有進行對齊的變數的時候會發生錯誤,那麼在這種架構下程式設計必須保證位元組對齊,但其他平臺可能沒有這種情況,但是最常見的是如果不按照適合其平臺要求對資料存放進行對齊,會在存取效率上帶來損失。

  比如有些平臺每次讀都是從偶地址開始,如果一個int型(假設為32位系統)如果存放在偶地址開始的地方,那麼一個讀週期就可以讀出這32bit,而如果存放在奇地址開始的地方,就需要2個讀週期,並對兩次讀出的結果的高低位元組進行拼湊才能得到該32bit資料。顯然在讀取效率上下降很多。

  另外位元組對齊的作用不僅是便於cpu快速訪問,同時合理的利用位元組對齊可以==有效地節省儲存空間==。

  也即CPU一次訪問時,要麼讀0x01~0x04,要麼讀0x05~0x08…硬體不支援一次訪問就讀到0x02~0x05

  例:如果0x02~0x05存了一個int,讀取這個int就需要先讀0x01~0x04,留下0x02~0x04的內容,再讀0x05~0x08,留下0x05的內容,兩部分拼接起來才能得到那個int的值,這樣讀一個int就要兩次記憶體訪問,效率就低了。

針對位元組對齊,我們在程式設計中如何考慮?

  如果在程式設計的時候要考慮節約空間的話,那麼我們只需要假定結構的首地址是0,然後各個變數按照上面的原則進行排列即可,基本的原則就是把結構中的變數按照型別大小從小到大宣告,儘量減少中間的填補空間,還有一種就是為了以空間換取時間的效率,我們顯示的進行填補空間進行對齊,比如:有一種使用空間換時間做法是顯式的插入reserved成員:

reserved成員對我們的程式沒有什麼意義,它只是起到填補空間以達到位元組對齊的目的,當然即使不加這個成員通常編譯器也會給我們自動填補對齊,我們自己加上它只是起到顯式的提醒作用。

struct A{
    char a;
    char reserved[3];    //使用空間換時間
    int b;
}

位元組對齊可能帶來的隱患

  程式碼中關於對齊的隱患,很多是隱式的,比如在強制型別轉換的時候,例如:

unsigned int i = 0x12345678;
unsigned char *p = NULL;
unsigned short *p1 = NULL;
 
p = &i;                            //這裡可假設p指向了一個4的整數倍地址,例如0x04
*p = 0x00;                         
p1 = (unsigned short *)(p + 1);    //這裡p1指向了p後移一位,也即為0x05
*p1 = 0x0000;

這樣最後兩句程式碼,從奇數邊界去訪問unsigned short型變數,顯然不符合對齊的規定,在x86上,類似的操作只會影響效率,但是在MIPS或者sparc上,可能就是一個error,因為它們要求必須位元組對齊。