1. 程式人生 > >淺談VC中的位元組對齊

淺談VC中的位元組對齊

   前幾天時,在公司和同事說到了位元組對齊,一直對這個概念比較模糊,只是在《程式設計師面試寶典》中看到過簡單的描述和一些面試題。後來在論壇中有看到有朋友在詢問位元組對齊的相關問題,自己也答不上來,覺得應該研究一下,所以就有了這一篇博文,是對學習的一個總結,也是對成長軌跡的一個記錄。

      位元組對齊,又叫記憶體對齊,個人理解就是一種C++中的型別在記憶體中空間分配策略。每一種型別儲存的起始地址,都要求是一個對齊模數(alignment modulus)的整數倍。問題來了,為什麼要有這種策略?計算中記憶體中的資料就是一個一個的位元組(byte),直接按照一個位元組一個位元組儲存就得了,為什麼還要那麼麻煩。把問題想簡單了。

      各個硬體平臺對儲存空間的處理上有很大的不同。一些平臺對某些特定型別的資料只能從某些特定地址開始存取。比如有些架構的CPU在訪問 一個沒有進行對齊的變數的時候會發生錯誤,那麼在這種架構下程式設計必須保證位元組對齊.其他平臺可能沒有這種情況,但是最常見的是如果不按照適合其平臺要求對 資料存放進行對齊,會在存取效率上帶來損失。

      計算機CPU一次處理可以處理多個位元組,就拿32位系統來說,CPU一次可以處理32bit的資料,也就是4個位元組。比如有些平臺每次讀都是從偶地址開始,假設有一個int型資料,存放在記憶體地址0x1的位置。CPU要讀取這個int資料,並且從地址0x0開始讀取資料。一次讀取4位元組,那麼這個int型還有一個位元組沒有讀到,就得再讀取一次剩下的那一個位元組,並且還要進行位操作,把兩次讀取的資料合併為一個int型資料。兩個字--麻煩,效率太低了。那怎麼辦呢?為了提高效率,乾脆在儲存的時候把這個int資料放在記憶體地址0x4的位置,0x1、0x2、0x3的位置都空著,CPU直接從0x4取資料,只需一次就取到了這個資料,還不用進行位操作。就是拿空間換時間,沒辦法,誰讓現在的存記憶體越來越大了呢?

      下面一些知識的總結,部分來自網際網路,感謝那些為C++奮鬥的兄弟。

 

位元組對齊的規則
      許多實際的計算機系統對基本型別資料在記憶體中存放的位置有限制,它們會要求這些資料的首地址的值是某個數k(通常它為4的倍數,這就是所謂的位元組對齊,而這個k則被稱為該資料型別的對齊模數(alignment modulus)。當一種型別S的對齊模數與另一種型別T的對齊模數的比值是大於1的整數,我們就稱型別S的對齊要求比T強(嚴格),而稱T比S弱(寬鬆)。下面來討論4種不同型別的對齊模數:

內建型別的自身對齊模數(有符號無符號相同)
          char 1

          short 2

          int    4

          float 4

          double 8

 

自定義型別的自身對齊模數(struct 、class)
          等同於其成員中最大的自身對齊模數

指定對齊模數
          我們給編譯器指定的對齊模數(在VC中使用指令:#pragma pack(n),如果不指定,在VS2010預設為8)

有效對齊模數
          指定對齊模數與型別自身對齊模數的較小的值,就是實際生效的對齊模數。

 

自定義型別中各個成員按照它們被宣告的順序在記憶體中順序儲存,第一個成員的地址和整個型別的地址相同
每個成員分別對齊,即每個成員按自己的方式對齊,並最小化長度,規則就是每個成員按其型別的對齊模數(通常是這個型別的大小)和指定對齊參模數中較小的一個對齊
結構、聯合或者類的資料成員,第一個放在偏移為0的地方,以後每個資料成員的對齊,按照指定對齊模數和這個資料成員自身長度兩個中比較小的那個進行。也就是說,當#pragma pack指定的值等於或者超過所有資料成員長度的時候,這個指定值的大小將不產生任何效果
自定義型別(如結構)整體的對齊(注意是“整體”)是按照結構體中長度最大的資料成員和指定對齊模數之間較小的那個值進行,這樣在成員是複雜型別時,可以最小化長度
結構整體長度的計算必須是成員的所有對齊模數數中最大的那個值的整數倍,不夠補空位元組,因為對齊引數都是2的n次方。這樣在處理陣列時可以保證每一項都邊界對齊
 

例如:

在上例中,假設起始地址0x0,那麼ch的地址為離0x0最近的且能被ch的有效對齊模數整除的地址,那麼就是0x0;以此類推,i的地址為0x4,sht的地址為0x8,alignment的地址與ch的地址一致。

VC中的位元組對齊設定
在VC IDE中,可以這樣修改:[Project]|[Settings],c/c++選項卡Category的Code Generation選項的Struct Member Alignment中修改,預設是8位元組。
在編碼時,可以使用指令動態修改:#pragma pack
指令#pragma pack
  

作用:指定結構體、聯合以及類成員的packing alignment;

語法:#pragma pack( [show] | [push | pop] [, identifier], n )

說明:

pack提供資料宣告級別的控制,對定義不起作用;
呼叫pack時不指定引數,n將被設成預設值;
一旦改變資料型別的alignment,直接效果就是佔用memory的減少,但是performance會下降;
 

語法具體分析:

show:可選引數;顯示當前packing aligment的位元組數,以warning message的形式被顯示;
push:可選引數;將當前指定的packing alignment數值進行壓棧操作,這裡的棧是the internal compiler stack,同時設定當前的packing alignment為n;如果n沒有指定,則將當前的packing alignment數值壓棧;
pop:可選引數;從internal compiler stack中刪除最頂端的record;如果沒有指定n,則當前棧頂record即為新的packing alignment數值;如果指定了n,則n將成為新的packing aligment數值;如果指定了identifier,則internal compiler stack中的record都將被pop直到identifier被找到,然後pop出identitier,同時設定packing alignment數值為當前棧頂的record;如果指定的identifier並不存在於internal compiler stack,則pop操作被忽略;
identifier:可選引數;當同push一起使用時,賦予當前被壓入棧中的record一個名稱;當同pop一起使用時,從internal compiler stack中pop出所有的record直到identifier被pop出,如果identifier沒有被找到,則忽略pop操作;
n:可選引數;指定packing的數值,以位元組為單位;預設數值是8,合法的數值分別是1、2、4、8、16。
使用示例:

程式中的位元組對齊與空間佔用
位元組對齊規則影響著struct和class的記憶體佔用。來看一個例子:

 

      程式中第2行#pragma pack (8)雖然指定了對齊模數為8,但是由於struct example1中的成員最大size為4(long變數size為4),故struct example1仍然按4位元組對齊,struct example1的size為8,即第22行的輸出結果;
  struct example2中包含了struct example1,其本身包含的簡單資料成員的最大size為2(short變數e),但是因為其包含了struct example1,而struct example1中的最大成員size為4,struct example2也應以4對齊,#pragma pack (8)中指定的對齊對struct example2也不起作用,故23行的輸出結果為16;
  由於struct example2中的成員以4為單位對界,故其char變數c後應補充3個空,其後才是成員struct1的記憶體空間,24行的輸出結果為4。

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

 

程式碼中關於對齊的隱患,很多是隱式的。比如在強制型別轉換的時候。例如:
unsigned int i = 0x12345678;
unsigned char *p=NULL;
unsigned short *p1=NULL;

p=&i;
*p=0x00;
p1=(unsigned short *)(p+1);
*p1=0x0000;
最後兩句程式碼,從奇數邊界去訪問unsignedshort型變數,顯然不符合對齊的規定。
在x86上,類似的操作只會影響效率,但是在MIPS或者sparc上,可能就是一個error,因為它們要求必須位元組對齊.

 

如果出現對齊或者賦值問題首先檢視

編譯器的big little端設定
看這種體系本身是否支援非對齊訪問
如果支援看設定了對齊與否,如果沒有則看訪問時需要加某些特殊的修飾來標誌其特殊訪問操作。
 

轉:https://blog.csdn.net/yunyun1886358/article/details/5651652