引言
考慮下面的結構體定義:
typedef
struct
t_foo;
int
main
(void
)
複製**
執行後輸出:
c1 ->
0, s ->
2, c2 ->
4, i ->
8
為什麼會這樣?這就是位元組對齊導致的問題。
一 什麼是位元組對齊
現代計算機中,記憶體空間按照位元組劃分,理論上可以從任何起始位址訪問任意型別的變數。但實際中在訪問特定型別變數時經常在特定的記憶體位址訪問,這就需要各種型別資料按照一定的規則在空間上排列,而不是順序乙個接乙個地存放,這就是對齊。
二 對齊的原因和作用
不同硬體平台對儲存空間的處理上存在很大的不同。某些平台對特定型別的資料只能從特定位址開始訪問,而不允許其在記憶體中任意存放。例如motorola 68000 處理器不允許16位的字存放在奇位址,否則會觸發異常,因此在這種架構下程式設計必須保證位元組對齊。
但最常見的情況是,如果不按照平台要求對資料存放進行對齊,會帶來訪問效率上的損失。比如32位的intel處理器通過匯流排訪問(包括讀和寫)記憶體資料。每個匯流排週期從偶位址開始訪問32位記憶體資料,記憶體資料以位元組為單位存放。如果乙個32位的資料沒有存放在4位元組整除的記憶體位址處,那麼處理器就需要2個匯流排週期對其進行訪問,顯然訪問效率下降很多。
因此,通過合理的記憶體對齊可以提高訪問效率。為使cpu能夠對資料進行快速訪問,資料的起始位址應具有「對齊」特性。比如4位元組資料的起始位址應位於4位元組邊界上,即起始位址能夠被4整除。
此外,合理利用位元組對齊還可以有效地節省儲存空間。但要注意,在32位機中使用1位元組或2位元組對齊,反而會降低變數訪問速度。因此需要考慮處理器型別。還應考慮編譯器的型別。在vc/c++和gnu gcc中都是預設是4位元組對齊。
三 對齊的分類和準則
主要基於intel x86架構介紹結構體對齊和棧記憶體對齊,位域本質上為結構體型別。
對於intel x86平台,每次分配記憶體應該是從4的整數倍位址開始分配,無論是對結構體變數還是簡單型別的變數。
3.1 結構體對齊
在c語言中,結構體是種復合資料型別,其構成元素既可以是基本資料型別(如int、long、float等)的變數,也可以是一些復合資料型別(如陣列、結構體、聯合等)的資料單元。編譯器為結構體的每個成員按照其自然邊界(alignment)分配空間。各成員按照它們被宣告的順序在記憶體中順序儲存,第乙個成員的位址和整個結構的位址相同。
位元組對齊的問題主要就是針對結構體。
3.1.1 簡單示例
先看個簡單的例子(32位,x86處理器,gcc編譯器):
【例1】設結構體如下定義:
複製**
struct a
;
struct b
;
複製**
已知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位元組。之所以出現上述結果,就是因為編譯器要對資料成員在空間上進行對齊。
3.1.2 對齊準則
先來看四個重要的基本概念:
1) 資料型別自身的對齊值:char型資料自身對齊值為1位元組,short型資料為2位元組,int/float型為4位元組,double型為8位元組。
2) 結構體或類的自身對齊值:其成員中自身對齊值最大的那個值。
3) 指定對齊值:#pragma pack (value)時的指定對齊值value。
4) 資料成員、結構體和類的有效對齊值:自身對齊值和指定對齊值中較小者,即有效對齊值=min。
基於上面這些值,就可以方便地討論具體資料結構的成員和其自身的對齊方式。
其中,有效對齊值n是最終用來決定資料存放位址方式的值。有效對齊n表示「對齊在n上」,即該資料的「存放起始位址%n=0」。而資料結構中的資料變數都是按定義的先後順序存放。第乙個資料變數的起始位址就是資料結構的起始位址。結構體的成員變數要對齊存放,結構體本身也要根據自身的有效對齊值圓整(即結構體成員變數占用總長度為結構體有效對齊值的整數倍)。
以此分析3.1.1節中的結構體b:
假設b從位址空間0x0000開始存放,且指定對齊值預設為4(4位元組對齊)。成員變數b的自身對齊值是1,比預設指定對齊值4小,所以其有效對齊值為1,其存放位址0x0000符合0x0000%1=0。成員變數a自身對齊值為4,所以有效對齊值也為4,只能存放在起始位址為0x0004~0x0007四個連續的位元組空間中,符合0x0004%4=0且緊靠第乙個變數。變數c自身對齊值為 2,所以有效對齊值也是2,可存放在0x00080x0009兩個位元組空間中,符合0x0008%2=0。所以從0x00000x0009存放的都是b內容。
再看資料結構b的自身對齊值為其變數中最大對齊值(這裡是b)所以就是4,所以結構體的有效對齊值也是4。根據結構體圓整的要求, 0x00000x0009=10位元組,(10+2)%4=0。所以0x0000a0x000b也為結構體b所占用。故b從0x0000到0x000b 共有12個位元組,sizeof(struct b)=12。
之所以編譯器在後面補充2個位元組,是為了實現結構陣列的訪問效率。試想如果定義乙個結構b的陣列,那麼第乙個結構起始位址是0沒有問題,但是第二個結構呢?按照陣列的定義,陣列中所有元素都緊挨著。如果我們不把結構體大小補充為4的整數倍,那麼下乙個結構的起始位址將是0x0000a,這顯然不能滿足結構的位址對齊。因此要把結構體補充成有效對齊大小的整數倍。其實對於char/short/int/float/double等已有型別的自身對齊值也是基於陣列考慮的,只是因為這些型別的長度已知,所以他們的自身對齊值也就已知。
上面的概念非常便於理解,不過個人還是更喜歡下面的對齊準則。
結構體位元組對齊的細節和具體編譯器實現相關,但一般而言滿足三個準則:
結構體變數的首位址能夠被其最寬基本型別成員的大小所整除;
結構體每個成員相對結構體首位址的偏移量(offset)都是成員大小的整數倍,如有需要編譯器會在成員之間加上填充位元組(internal adding);
結構體的總大小為結構體最寬基本型別成員大小的整數倍,如有需要編譯器會在最末乙個成員之後加上填充位元組。
對於以上規則的說明如下:
第一條:編譯器在給結構體開闢空間時,首先找到結構體中最寬的基本資料型別,然後尋找記憶體位址能被該基本資料型別所整除的位置,作為結構體的首位址。將這個最寬的基本資料型別的大小作為上面介紹的對齊模數。
第二條:為結構體的乙個成員開闢空間之前,編譯器首先檢查預開闢空間的首位址相對於結構體首位址的偏移是否是本成員大小的整數倍,若是,則存放本成員,反之,則在本成員和上乙個成員之間填充一定的位元組,以達到整數倍的要求,也就是將預開闢空間的首位址後移幾個位元組。
第三條:結構體總大小是包括填充位元組,最後乙個成員滿足上面兩條以外,還必須滿足第三條,否則就必須在最後填充幾個位元組以達到本條要求。
【例2】假設4位元組對齊,以下程式的輸出結果是多少?
/* offset巨集定義可取得指定結構體某成員在結構體內部的偏移 */
#define offset(st, field) (size_t)&(((st*)0)->field)
typedef
struct
t_test;
intmain
(void
)
執行後輸出如下:
size = 16
a-0, b-2, c-4, d-8
e[0]-12, e[1]-13, e[2]-14
下面來具體分析:
首先char a占用1個位元組,沒問題。
short b本身占用2個位元組,根據上面準則2,需要在b和a之間填充1個位元組。
char c占用1個位元組,沒問題。
int d本身占用4個位元組,根據準則2,需要在d和c之間填充3個位元組。
char e[3];本身占用3個位元組,根據原則3,需要在其後補充1個位元組。
因此,sizeof(t_test) = 1 + 1 + 2 + 1 + 3 + 4 + 3 + 1 = 16位元組。
C語言位元組對齊詳解
一 什麼是對齊,以及為什麼要對齊 1.現代計算機中記憶體空間都是按照byte劃分的,從理論上講似乎對任何型別的變數的訪問可以從任何位址開始,但實際情況是在訪問特定變數的時候經常在特定的記憶體位址訪問,這就需要各型別資料按照一定的規則在空間上排列,而不是順序的乙個接乙個的排放,這就是對齊。2.對齊的作...
C語言位元組對齊詳解
一 什麼是對齊,以及為什麼要對齊 1.現代計算機中記憶體空間都是按照byte劃分的,從理論上講似乎對任何型別的變數的訪問可以從任何位址開始,但實際情況是在訪問特定變數的時候經常在特定的記憶體位址訪問,這就需要各型別資料按照一定的規則在空間上排列,而不是順序的乙個接乙個的排放,這就是對齊。2.對齊的作...
詳解C語言位元組對齊
一 什麼是對齊,以及為什麼要對齊 1.現代計算機中記憶體空間都是按照byte劃分的,從理論上講似乎對任何型別的變數的訪問可以從任何位址開始,但實際情況是在訪問特定變數的時候經常在特定的記憶體位址訪問,這就需要各型別資料按照一定的規則在空間上排列,而不是順序的乙個接乙個的排放,這就是對齊。2.對齊的作...