理一理C語言位元組對齊的那些事
前言
位元組對齊是我們初學C語言就會接觸到的一個概念,但是到底什麼是位元組對齊?對齊準則又是什麼?為什麼要位元組對齊呢?位元組對齊對我們程式設計有什麼啟示?本文將簡單理一理位元組對齊的那些事。
什麼是位元組對齊
計算機中記憶體大小的基本單位是位元組(byte),理論上來講,可以從任意地址訪問某種基本資料型別,但是實際上,計算機並非逐位元組大小讀寫記憶體,而是以2,4,或8的 倍數的位元組塊來讀寫記憶體,如此一來就會對基本資料型別的合法地址作出一些限制,即它的地址必須是2,4或8的倍數。那麼就要求各種資料型別按照一定的規則在空間上排列,這就是對齊。
對齊準則是什麼
總的來說,位元組對齊有以下準則:
-
結構體變數的首地址能夠被其最大基本型別成員位元組數大小所整除。
-
結構體每個成員相對結構體首地址的偏移都是成員大小的整數倍,如不滿足,對前一個成員填充位元組以滿足。
-
結構體的總大小為結構體對最大成員大小的整數倍,如不滿足,最後填充位元組以滿足。
我們通過一個小例子來說明是如何對齊的。
考慮下面的程式:
/*================================================================ *Copyright (C) 2018Ltd. All rights reserved. * *檔名稱:testByteAlign.c *創 建 者:shouwang *建立日期:2018年09月15日 *描述: * ================================================================*/ #include<stdio.h> #include<stdint.h> struct test { int a; char b; int c; short d; }; int main(int argc,char *argv) { /*在32位和64位的機器上,size_t的大小不同*/ printf("the size of struct test is %zu\n",sizeof(struct test)); return 0; }
編譯成32位程式並執行(預設四位元組自然對齊),可以看到,結構體test 的大小為16位元組,而不是11位元組(a佔4位元組,b佔1位元組,c佔4位元組,d佔2位元組)
#64位機器上編譯32位程式可能需要安裝一個庫 #sudo apt-get install gcc-multilib gcc -m32 -o testByteAlign testByteAlign.c #編譯程式 chmod +x testByteAlign#賦執行許可權 ./testByteAlign#執行 the size of struct test is 16
實際上,結構體test的成員在記憶體中可能是像下面這樣分佈的(數值為偏移量)
未對齊時:
0~3 | 4 | 5~9 | 10~11 |
---|---|---|---|
a | b | c | d |
對齊時:
0~3 | 4 | 5~7 | 8~11 | 12~13 | 14~15 |
---|---|---|---|---|---|
a | b | 填充內容 | c | d | 填充內容 |
從上面可以看出,c的偏移為5,不滿足對齊要求(它的偏移量應該能夠被sizeof(int)大小整除),因此在b後面填充了3個位元組,使得c的偏移為8。在b後面填充後,d已經滿足對齊要求了,為什麼最後還要填充位元組呢?或者說,為什麼需要滿足第三條準則呢?
考慮下面的宣告
struct teArray[2];
我們不難知道,teArray[0]的d如果不填充位元組,那麼teArray[1]的a偏移為14,不滿足對齊要求,因此d後面也需要填充位元組。
為什麼要位元組對齊
無論資料是否對齊,大多數計算機還是能夠正確工作,而且從前面可以看到,結構體test本來只需要11位元組的空間,最後卻佔用了16位元組,很明顯 浪費了空間 ,那麼為什麼還要進行位元組對齊呢?最重要的考慮是 提高記憶體系統性能
前面我們也說到,計算機每次讀寫一個位元組塊,例如,假設計算機總是從記憶體中取8個位元組,如果一個double資料的地址對齊成8的倍數,那麼一個記憶體操作就可以讀或者寫,但是如果這個double資料的地址沒有對齊,資料就可能被放在兩個8位元組塊中,那麼我們可能需要執行兩次記憶體訪問,才能讀寫完成。顯然在這樣的情況下,是低效的。所以需要位元組對齊來提高記憶體系統性能。
在有些處理器中,如果需要未對齊的資料,可能不能夠正確工作甚至crash,這裡我們不多討論。
實際程式設計中的考慮
實際上,位元組對齊的細節都由編譯器來完成,我們不需要特意進行位元組的對齊,但並不意味著我們不需要關注位元組對齊的問題。
空間儲存
還是考慮前面的結構體test,其佔用空間大小為16位元組,但是如果我們換一種宣告方式,調整變數的順序,重新執行程式,最後發現結構體test佔用大小為 12位元組
struct test { int a; char b; short d; int c; };
空間儲存情況如下,b和c儲存在了一個位元組快中:
0~3 | 4 | 5 | 6~7 | 8~11 |
---|---|---|---|---|
a | b | 填充內容 | c | d |
也就是說,如果我們在設計結構的時候,合理調整成員的位置,可以大大節省儲存空間。但是需要在空間和可讀性之間進行權衡。
跨平臺通訊
由於不同平臺對齊方式可能不同,如此一來,同樣的結構在不同的平臺其大小可能不同,在無意識的情況下,互相傳送的資料可能出現錯亂,甚至引發嚴重的問題。因此,為了不同處理器之間能夠正確的處理訊息,我們有兩種可選的處理方法。
-
1位元組對齊
-
自己對結構進行位元組填充
我們可以使用偽指令#pragma pack(n)(n為位元組對齊數)來使得結構間一位元組對齊。
同樣是前面的程式,如果在結構體test的前面加上偽指令,即如下:
#pragma pack(1) /*1位元組對齊*/ struct test { int a; char b; int c; short d; }; #pragma pack()/*還原預設對齊*/
在這樣的宣告下,任何平臺結構體test的大小都為11位元組,這樣做能夠保證跨平臺的結構大小一致,同時還節省了空間,但不幸的是,降低了效率。
當然了對於單個結構體,如下的方法,使其1位元組對齊
struct test { int a; char b; int c; short d; }__attribute__ ((packed));
注:
-
__attribute__((aligned (n))),讓所作用的結構成員對齊在n位元組自然邊界上。如果結構中有成員的長度大於n,則按照最大成員的長度來對齊。
-
__attribute__ ((packed)),取消結構在編譯過程中的優化對齊,也可以認為是1位元組對齊。
除了前面的1位元組對齊,還可以進行人為的填充,即test結構體宣告如下:
struct test { int a; char b; char reserve[3]; int c; short d; char reserve1[2]; };
訪問效率高,但並不節省空間,同時擴充套件性不是很好,例如,當位元組對齊有變化時,需要填充的位元組數可能就會發生變化。
總結
雖然我們不需要具體關心位元組對齊的細節,但是如果不關注位元組對齊的問題,可能會在程式設計中遇到難以理解或解決的問題。因此針對位元組對齊,總結了以下處理建議:
-
結構體成員合理安排位置,以節省空間
-
跨平臺資料結構可考慮1位元組對齊,節省空間但影響訪問效率
-
跨平臺資料結構人為進行位元組填充,提高訪問效率但不節省空間
-
本地資料採用預設對齊,以提高訪問效率
-
32位與64位預設對齊數不一樣
思考
下面的結構體使用sizeof得到的大小是多少?
struct test { char a; char b; };
關注公眾號【程式設計珠璣】,獲取更多Linux/C/C++/Python/Go/演算法/工具等原創技術文章。 後臺免費獲取經典電子書和視訊資源