記憶體對齊與ANSI C中struct型資料的記憶體布局

來源:互聯網
上載者:User

【引用自:http://blog.csdn.net/soloist/archive/2004/12/12/213717.aspx】

    當在C中定義了一個結構類型時,它的大小是否等於各欄位(field)大小之和?編譯器將如何在記憶體中放置這些欄位?ANSI C對結構體的記憶體布局有什麼要求?而我們的程式又能否依賴這種布局?這些問題或許對不少朋友來說還有點模糊,那麼本文就試著探究它們背後的秘密。

    首先,至少有一點可以肯定,那就是ANSI C保證結構體中各欄位在記憶體中出現的位置是隨它們的聲明順序依次遞增的,並且第一個欄位的首地址等於整個結構體執行個體的首地址。比如有這樣一個結構體:
 
  struct vector{int x,y,z;} s;
  int *p,*q,*r;
  struct vector *ps;
 
  p = &s.x;
  q = &s.y;
  r = &s.z;
  ps = &s;

  assert(p < q);
  assert(p < r);
  assert(q < r);
  assert((int*)ps == p);
  // 上述斷言一定不會失敗

    這時,有朋友可能會問:"標準是否規定相鄰欄位在記憶體中也相鄰?"。 唔,對不起,ANSI C沒有做出保證,你的程式在任何時候都不應該依賴這個假設。那這是否意味著我們永遠無法勾勒出一幅更清晰更精確的結構體記憶體布局圖?哦,當然不是。不過先讓我們從這個問題中暫時抽身,關注一下另一個重要問題————記憶體對齊。

    許多實際的電腦系統對基本類型資料在記憶體中存放的位置有限制,它們會要求這些資料的首地址的值是某個數k(通常它為4或8)的倍數,這就是所謂的記憶體對齊,而這個k則被稱為該資料類型的對齊模數(alignment modulus)。當一種類型S的對齊模數與另一種類型T的對齊模數的比值是大於1的整數,我們就稱類型S的對齊要求比T強(嚴格),而稱T比S弱(寬鬆)。這種強制的要求一來簡化了處理器與記憶體之間傳輸系統的設計,二來可以提升讀取資料的速度。比如這麼一種處理器,它每次讀寫記憶體的時候都從某個8倍數的地址開始,一次讀出或寫入8個位元組的資料,假如軟體能保證double類型的資料都從8倍數地址開始,那麼讀或寫一個double類型資料就只需要一次記憶體操作。否則,我們就可能需要兩次記憶體操作才能完成這個動作,因為資料或許恰好橫跨在兩個符合對齊要求的8位元組記憶體塊上。某些處理器在資料不滿足對齊要求的情況下可能會出錯,但是Intel的IA32架構的處理器則不管資料是否對齊都能正確工作。不過Intel奉勸大家,如果想提升效能,那麼所有的程式資料都應該儘可能地對齊。Win32平台下的微軟C編譯器(cl.exe for 80x86)在預設情況下採用如下的對齊規則: 任何基礎資料型別 (Elementary Data Type)T的對齊模數就是T的大小,即sizeof(T)。比如對於double類型(8位元組),就要求該類型資料的地址總是8的倍數,而char類型資料(1位元組)則可以從任何一個地址開始。Linux下的GCC奉行的是另外一套規則(在資料中查得,並未驗證,如錯誤請指正):任何2位元組大小(包括單位元組嗎?)的資料類型(比如short)的對齊模數是2,而其它所有超過2位元組的資料類型(比如long,double)都以4為對齊模數。

    現在回到我們關心的struct上來。ANSI C規定一種結構類型的大小是它所有欄位的大小以及欄位之間或欄位尾部的填充區大小之和。嗯?填充區?對,這就是為了使結構體欄位滿足記憶體對齊要求而額外分配給結構體的空間。那麼結構體本身有什麼對齊要求嗎?有的,ANSI C標準規定結構體類型的對齊要求不能比它所有欄位中要求最嚴格的那個寬鬆,可以更嚴格(但此非強制要求,VC7.1就僅僅是讓它們一樣嚴格)。我們來看一個例子(以下所有實驗的環境是Intel Celeron 2.4G + WIN2000 PRO + vc7.1,記憶體對齊編譯選項是"預設",即不指定/Zp與/pack選項):

  typedef struct ms1
  {
     char a;
     int b;
  } MS1;

    假設MS1按如下方式記憶體布局(本文所有中的記憶體位址從左至右遞增):
       _____________________________
       |       |                   |
       |   a   |        b          |
       |       |                   |
       +---------------------------+
 Bytes:    1             4

    因為MS1中有最強對齊要求的是b欄位(int),所以根據編譯器的對齊規則以及ANSI C標準,MS1對象的首地址一定是4(int類型的對齊模數)的倍數。那麼上述記憶體布局中的b欄位能滿足int類型的對齊要求嗎?嗯,當然不能。如果你是編譯器,你會如何巧妙安排來滿足CPU的癖好呢?呵呵,經過1毫秒的艱苦思考,你一定得出了如下的方案:

       _______________________________________
       |       |"""""""""""|                 |
       |   a   |""padding""|       b         |
       |       |"""""""""""|                 |
       +-------------------------------------+
 Bytes:    1         3             4

    這個方案在a與b之間多分配了3個填充(padding)位元組,這樣當整個struct對象首地址滿足4位元組的對齊要求時,b欄位也一定能滿足int型的4位元組對齊規定。那麼sizeof(MS1)顯然就應該是8,而b欄位相對於結構體首地址的位移就是4。非常好理解,對嗎?現在我們把MS1中的欄位交換一下順序:

  typedef struct ms2
  {
     int a;
     char b;
  } MS2;

    或許你認為MS2比MS1的情況要簡單,它的布局應該就是

       _______________________
       |             |       |
       |     a       |   b   |
       |             |       |
       +---------------------+
 Bytes:      4           1

    因為MS2對象同樣要滿足4位元組對齊規定,而此時a的地址與結構體的首地址相等,所以它一定也是4位元組對齊。嗯,分析得有道理,可是卻不全面。讓我們來考慮一下定義一個MS2類型的數組會出現什麼問題。C標準保證,任何類型(包括自訂結構類型)的數組所佔空間的大小一定等於一個單獨的該類型資料的大小乘以數組元素的個數。換句話說,數組各元素之間不會有空隙。按照上面的方案,一個MS2數組array的布局就是:

|<-    array[1]     ->|<-    array[2]     ->|<- array[3] .....

__________________________________________________________
|             |       |              |      |
|     a       |   b   |      a       |   b  |.............
|             |       |              |      |
+----------------------------------------------------------
Bytes:  4         1          4           1

    當數組首地址是4位元組對齊時,array[1].a也是4位元組對齊,可是array[2].a呢?array[3].a ....呢?可見這種方案在定義結構體數組時無法讓數組中所有元素的欄位都滿足對齊規定,必須修改成如下形式:

       ___________________________________
       |             |       |"""""""""""|
       |     a       |   b   |""padding""|
       |             |       |"""""""""""|
       +---------------------------------+
 Bytes:      4           1         3

    現在無論是定義一個單獨的MS2變數還是MS2數組,均能保證所有元素的所有欄位都滿足對齊規定。那麼sizeof(MS2)仍然是8,而a的位移為0,b的位移是4。

    好的,現在你已經掌握了結構體記憶體布局的基本準則,嘗試分析一個稍微複雜點的類型吧。

  typedef struct ms3
  {
     char a;
     short b;
     double c;
  } MS3;

    我想你一定能得出如下正確的布局圖:
        
        padding 
           |
      _____v_________________________________
      |   |"|     |"""""""""|               |
      | a |"|  b  |"padding"|       c       |
      |   |"|     |"""""""""|               |
      +-------------------------------------+
Bytes:  1  1   2       4            8
          
    sizeof(short)等於2,b欄位應從偶數地址開始,所以a的後面填充一個位元組,而sizeof(double)等於8,c欄位要從8倍數地址開始,前面的a、b欄位加上填充位元組已經有4 bytes,所以b後面再填充4個位元組就可以保證c欄位的對齊要求了。sizeof(MS3)等於16,b的位移是2,c的位移是8。接著看看結構體中欄位還是結構類型的情況:

  typedef struct ms4
  {
     char a;
     MS3 b;
  } MS4;

    MS3中記憶體要求最嚴格的欄位是c,那麼MS3類型資料的對齊模數就與double的一致(為8),a欄位後面應填充7個位元組,因此MS4的布局應該是:
       _______________________________________
       |       |"""""""""""|                 |
       |   a   |""padding""|       b         |
       |       |"""""""""""|                 |
       +-------------------------------------+
 Bytes:    1         7             16

    顯然,sizeof(MS4)等於24,b的位移等於8。

    在實際開發中,我們可以通過指定/Zp編譯選項來更改編譯器的對齊規則。比如指定/Zpn(VC7.1中n可以是1、2、4、8、16)就是告訴編譯器最大對齊模數是n。在這種情況下,所有小於等於n位元組的基礎資料型別 (Elementary Data Type)的對齊規則與預設的一樣,但是大於n個位元組的資料類型的對齊模數被限制為n。事實上,VC7.1的預設對齊選項就相當於/Zp8。仔細看看MSDN對這個選項的描述,會發現它鄭重告誡了程式員不要在MIPS和Alpha平台上用/Zp1和/Zp2選項,也不要在16位平台上指定/Zp4和/Zp8(想想為什嗎?)。改變編譯器的對齊選項,對照程式運行結果重新分析上面4種結構體的記憶體布局將是一個很好的複習。

    到了這裡,我們可以回答本文提出的最後一個問題了。結構體的記憶體布局依賴於CPU、作業系統、編譯器及編譯時間的對齊選項,而你的程式可能需要運行在多種平台上,你的原始碼可能要被不同的人用不同的編譯器編譯(試想你為別人提供一個開放源碼的庫),那麼除非絕對必需,否則你的程式永遠也不要依賴這些詭異的記憶體布局。順便說一下,如果一個程式中的兩個模組是用不同的對齊選項分別編譯的,那麼它很可能會產生一些非常微妙的錯誤。如果你的程式確實有很難理解的行為,不防仔細檢查一下各個模組的編譯選項。

    思考題:請分析下面幾種結構體在你的平台上的記憶體布局,並試著尋找一種合理安排欄位聲明順序的方法以盡量節省記憶體空間。

    A. struct P1 { int a; char b; int c; char d; };
    B. struct P2 { int a; char b; char c; int d; };
    C. struct P3 { short a[3]; char b[3]; };
    D. struct P4 { short a[3]; char *b[3]; };
    E. struct P5 { struct P2 *a; char b; struct P1 a[2];  };

參考資料:

    【1】《深入理解電腦系統(修訂版)》,
         (著)Randal E.Bryant; David O'Hallaron,
         (譯)龔奕利 雷迎春,
         中國電力出版社,2004
   
    【2】《C: A Reference Manual》(影印版),
         (著)Samuel P.Harbison; Guy L.Steele,
         人民郵電出版社,2003

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.