github上有大神翻譯了一篇內存對齊的英文文獻,我復現了一下過程;
發現其中有個地方有出入(strcut foo6{}),因此特地查了下文獻,做了下修正,記錄如下。
1、原文
作者:Eric S. Raymond
原文鏈接:http://www.catb.org/esr/structure-packing/
誰應閱讀本文
本文探討如何通過手工重新打包C結構體聲明,來減小內存空間占用。你需要掌握基本的C語言知識,以理解本文所講述的內容。
如果你在內存容量受限的嵌入式系統中寫程序,或者編寫操作系統內核代碼,就有必要了解這項技術。如果數據集巨大,應用時常逼近內存極限,這項技術會有所幫助。倘若你非常非常關心如何最大限度地減少處理器緩存段(cache-line)未命中情況的發生,這項技術也有所裨益。
最后,理解這項技術是通往其他C語言艱深話題的門徑。若不掌握,就算不上高級C程序員。當你自己也能寫出這樣的文檔,并且有能力明智地評價它之后,才稱得上C語言大師。
緣何寫作本文
2013年底,我【當然指的是原作大觸啊!】大量應用了一項C語言優化技術,這項技術是我早在二十余年前就已掌握的,但彼時之后,鮮有使用。
我需要減少一個程序對內存空間的占用,它使用了上千(有時甚至幾十萬)C結構體實例。這個程序是cvs-fast-export,在將其應用于大規模軟件倉庫時,程序會出現內存耗盡錯誤。
通過精心調整結構成體員的順序,可以在這種情況下大幅減少內存占用。其效果顯著——在上述案例中,可以減少40%的內存空間。程序應用于更大的軟件倉庫,也不會因內存耗盡而崩潰。
但隨著工作展開,我意識到這項技術在近些年幾乎已被遺忘。Web搜索證實了我的想法,現今的C程序員們似乎已不再談論這些話題,至少從搜索引擎中看不到。維基百科有些條目涉及這一主題,但未曾有人完整闡述。
事出有因。計算機科學課程(正確地)引導人們遠離微觀優化,轉而尋求更理想的算法。計算成本一路走低,令壓榨內存的必要性變得越來越低。舊日里,黑客們通過在陌生的硬件架構中跌跌撞撞學習——如今已不多見。
然而這項技術在關鍵時刻仍頗具價值,并且只要內存容量有限,價值就始終存在。本文意在節省C程序員重新發掘這項技術所需的時間,讓他們有精力關注更重要任務。
對齊要求
首先需要了解的是,對于現代處理器,C編譯器在內存中放置基本C數據類型的方式受到約束,以令內存的訪問速度更快。
在x86或ARM處理器中,基本C數據類型通常并不存儲于內存中的隨機字節地址。實際情況是,除char外,所有其他類型都有“對齊要求”:char可起始于任意字節地址,2字節的short必須從偶數字節地址開始,4字節的int或float必須從能被4整除的地址開始,8比特的long和double必須從能被8整除的地址開始。無論signed(有符號)還是unsigned(無符號)都不受影響。
用行話來說,x86和ARM上的基本C類型是“自對齊(self-aligned)”的。關于指針,無論32位(4字節)還是64位(8字節)也都是自對齊的。
自對齊可令訪問速度更快,因為它有利于生成單指令(single-instruction)存取這些類型的數據。另一方面,如若沒有對齊約束,可能最終不得不通過兩個或更多指令訪問跨越機器字邊界的數據。字符數據是種特殊情況,因其始終處在單一機器字中,所以無論存取何處的字符數據,開銷都是一致的。這也就是它不需要對齊的原因。
我提到“現代處理器”,是因為有些老平臺強迫C程序違反對齊規則(例如,為int指針分配一個奇怪的地址并試圖使用它),不僅令速度減慢,還會導致非法指令錯誤。例如Sun SPARC芯片就有這種問題。事實上,如果你下定決心,并恰當地在處理器中設置標志位(e18),在x86平臺上,也能引發這種錯誤。
另外,自對齊并非唯一規則。縱觀歷史,有些處理器,由其是那些缺乏桶式移位器(Barrel shifter)的處理器限制更多。如果你從事嵌入式系統領域編程,有可能掉進這些潛伏于草叢之中的陷阱。小心這種可能。
你還可以通過pragma指令(通常為#pragma pack
)強迫編譯器不采用處理器慣用的對齊規則。但請別隨意運用這種方式,因為它強制生成開銷更大、速度更慢的代碼。通常,采用我在下文介紹的方式,可以節省相同或相近的內存。
使用#pragma pack的唯一理由是——假如你需讓C語言的數據分布,與某種位級別的硬件或協議完全匹配(例如內存映射硬件端口),而違反通用對齊規則又不可避免。如果你處于這種困境,且不了解我所講述的內容,那你已深陷泥潭,祝君好運。
填充
我們來看一個關于變量在內存中分布的簡單案例。思考形式如下的一系列變量聲明,它們處在一個C模塊的頂層。
char *p;
char c;
int x;
假如你對數據對齊一無所知,也許以為這3個變量將在內存中占據一段連續空間。也就是說,在32位系統上,一個4字節指針之后緊跟著1字節的char,其后又緊跟著4字節int。在64位系統中,唯一的區別在于指針將占用8字節。
然而實際情況(在x86、ARM或其他采用自對齊類型的平臺上)如下。存儲p需要自對齊的4或8字節空間,這取決于機器字的大小。這是指針對齊——極其嚴格。
c緊隨其后,但接下來x的4字節對齊要求,將強制在分布中生成了一段空白,仿佛在這段代碼中插入了第四個變量,如下所示。
char *p; /* 4 or 8 bytes */
char c; /* 1 byte */
char pad[3]; /* 3 bytes */
int x; /* 4 bytes */
字符數組pad[3]
意味著在這個結構體中,有3個字節的空間被浪費掉了。老派術語將其稱之為“廢液(slop)”。
如果x為2字節short:
char *p;
char c;
short x;
在這個例子中,實際分布將會是:
char *p; /* 4 or 8 bytes */
char c; /* 1 byte */
char pad[1]; /* 1 byte */
short x; /* 2 bytes */
另一方面,如果x為64位系統中的long:
char *p;
char c;
long x;
我們將得到:
char *p; /* 8 bytes */
char c; /* 1 byte */
char pad[7]; /* 7 bytes */
long x; /* 8 bytes */
若你一路仔細讀下來,現在可能會思索,何不首先聲明較短的變量?
char c;
char *p;
int x;
假如實際內存分布可以寫成下面這樣:
char c;
char pad1[M];
char *p;
char pad2[N];
int x;
那M
與N
分別為幾何?
首先,在此例中,N
將為0,x
的地址緊隨p
之后,能確保是與指針對齊的,因為指針的對齊要求總比int嚴格。
M
的值就不易預測了。編譯器若是恰好將c
映射為機器字的最后一個字節,那么下一個字節(p
的第一個字節)將恰好由此開始,并恰好與指針對齊。這種情況下,M
將為0。
不過更有可能的情況是,c
將被映射為機器字的首字節。于是乎M
將會用于填充,以使p
指針對齊——32位系統中為3字節,64位系統中為7字節。
中間情況也有可能發生。M的值有可能在0到7之間(32位系統為0到3),因為char可以從機器字的任何位置起始。
倘若你希望這些變量占用的空間更少,那么可以交換x
與c
的次序。
char *p; /* 8 bytes */
long x; /* 8 bytes */
char c; /* 1 byte */
通常,對于C代碼中的少數標量變量(scalar variable),采用調換聲明次序的方式能節省幾個有限的字節,效果不算明顯。而將這種技術應用于非標量變量(nonscalar variable)——尤其是結構體,則要有趣多了。
在講述這部分內容前,我們先對標量數組做個說明。在具有自對齊類型的平臺上,char、short、int、long和指針數組都沒有內部填充,每個成員都與下一個成員自動對齊。
在下一節我們將會看到,這種情況對結構體數組并不適用。
結構體的對齊和填充
通常情況下,結構體實例以其最寬的標量成員為基準進行對齊。編譯器之所以如此,是因為此乃確保所有成員自對齊,實現快速訪問最簡便的方法。
此外,在C語言中,結構體的地址,與其第一個成員的地址一致——不存在頭填充(leading padding)。小心:在C++中,與結構體相似的類,可能會打破這條規則!(是否真的如此,要看基類和虛擬成員函數是如何實現的,與不同的編譯器也有關聯。)
假如你對此有疑惑,ANSI C提供了一個offsetof()
宏,可用于讀取結構體成員位移。
考慮這個結構體:
struct foo1 {
char *p;
char c;
long x;
};
假定處在64位系統中,任何struct fool
的實例都采用8字節對齊。不出所料,其內存分布將會像下面這樣:
struct foo1 {
char *p; /* 8 bytes */
char c; /* 1 byte */
char pad[7]; /* 7 bytes */
long x; /* 8 bytes */
};
看起來仿佛與這些類型的變量單獨聲明別無二致。但假如我們將c
放在首位,就會發現情況并非如此。
struct foo2 {
char c; /* 1 byte */
char pad[7]; /* 7 bytes */
char *p; /* 8 bytes */
long x; /* 8 bytes */
};
如果成員是互不關聯的變量,c
便可能從任意位置起始,pad
的大小則不再固定。因為struct foo2
的指針需要與其最寬的成員為基準對齊,這變得不再可能。現在c
需要指針對齊,接下來填充的7個字節被鎖定了。
現在,我們來談談結構體的尾填充(trailing padding)。為了解釋它,需要引入一個基本概念,我將其稱為結構體的“跨步地址(stride address)”。它是在結構體數據之后,與結構體對齊一致的首個地址。
結構體尾填充的通用法則是:編譯器將會對結構體進行尾填充,直至它的跨步地址。這條法則決定了sizeof()
的返回值。
考慮64位x86或ARM系統中的這個例子:
struct foo3 {
char *p; /* 8 bytes */
char c; /* 1 byte */
};
struct foo3 singleton;
struct foo3 quad[4];
你以為sizeof(struct foo3)
的值是9,但實際是16。它的跨步地址是(&p)[2]
。于是,在quad
數組中,每個成員都有7字節的尾填充,因為下個結構體的首個成員需要在8字節邊界上對齊。內存分布就好像這個結構是這樣聲明的:
struct foo3 {
char *p; /* 8 bytes */
char c; /* 1 byte */
char pad[7];
};
作為對比,思考下面的例子:
struct foo4 {
short s; /* 2 bytes */
char c; /* 1 byte */
};
因為s
只需要2字節對齊,跨步地址僅在c
的1字節之后,整個struct foo4
也只需要1字節的尾填充。形式如下:
struct foo4 {
short s; /* 2 bytes */
char c; /* 1 byte */
char pad[1];
};
sizeof(struct foo4)
的返回值將為4。
現在我們考慮位域(bitfields)。利用位域,你能聲明比字符寬度更小的成員,低至1位,例如:
struct foo5 {
short s;
char c;
int flip:1;
int nybble:4;
int septet:7;
};
關于位域需要了解的是,它們是由字(或字節)層面的掩碼和移位指令實現的。從編譯器的角度來看,struct foo5
中的位域就像2字節、16位的字符數組,只用到了其中12位。為了使結構體的長度是其最寬成員長度sizeof(short)
的整數倍,接下來進行了填充。
==這里存疑==。
struct foo5 {
short s; /* 2 bytes */
char c; /* 1 byte */
int flip:1; /* total 1 bit */
int nybble:4; /* total 5 bits */
int septet:7; /* total 12 bits */
int pad1:4; /* total 16 bits = 2 bytes */
char pad2; /* 1 byte */
};
這是最后一個重要細節:如果你的結構體中含有結構體成員,內層結構體也要和最長的標量有相同的對齊。假如你寫下了這段代碼:
struct foo6 {
char c;
struct foo5 {
char *p;
short x;
} inner;
};
內層結構體成員char *p
強迫外層結構體與內層結構體指針對齊一致。在64位系統中,實際的內存分布將類似這樣:
struct foo6 {
char c; /* 1 byte */
char pad1[7]; /* 7 bytes */
struct foo6_inner {
char *p; /* 8 bytes */
short x; /* 2 bytes */
char pad2[6]; /* 6 bytes */
} inner;
};
它啟示我們,能通過重新打包節省空間。24個字節中,有13個為填充,浪費了超過50%的空間!
結構體成員重排
理解了編譯器在結構體中間和尾部插入填充的原因與方式后,我們來看看如何榨出這些廢液。此即結構體打包的技藝。
首先注意,廢液只存在于兩處。其一是較大的數據類型(需要更嚴格的對齊)跟在較小的數據類型之后。其二是結構體自然結束的位置在跨步地址之前,這里需要填充,以使下個結構體能正確地對齊。
消除廢液最簡單的方式,是按對齊值遞減重新對結構體成員排序。即讓所有指針對齊成員排在最前面,因為在64位系統中它們占用8字節;然后是4字節的int;再然后是2字節的short,最后是字符。
因此,以簡單的鏈表結構體為例:
struct foo7 {
char c;
struct foo7 *p;
short x;
};
將隱含的廢液寫明,形式如下:
struct foo7 {
char c; /* 1 byte */
char pad1[7]; /* 7 bytes */
struct foo7 *p; /* 8 bytes */
short x; /* 2 bytes */
char pad2[6]; /* 6 bytes */
};
總共是24字節。如果按長度重排,我們得到:
struct foo8 {
struct foo8 *p;
short x;
char c;
};
考慮到自對齊,我們看到所有數據域之間都不需填充。因為有較嚴對齊要求(更長)成員的跨步地址對不太嚴對齊要求的(更短)成員來說,總是合法的對齊地址。重打包過的結構體只需要尾填充:
struct foo8 {
struct foo8 *p; /* 8 bytes */
short x; /* 2 bytes */
char c; /* 1 byte */
char pad[5]; /* 5 bytes */
};
重新打包將空間降為16字節。也許看起來不算很多,但假如這個鏈表的長度有20萬呢?將會積少成多。
注意,重新打包不能確保在所有情況下都能節省空間。將這項技術應用于更靠前struct foo6
的那個例子,我們得到:
struct foo9 {
struct foo9_inner {
char *p; /* 8 bytes */
int x; /* 4 bytes */
} inner;
char c; /* 1 byte */
};
將填充寫明:
struct foo9 {
struct foo9_inner {
char *p; /* 8 bytes */
int x; /* 4 bytes */
char pad[4]; /* 4 bytes */
} inner;
char c; /* 1 byte */
char pad[7]; /* 7 bytes */
};
結果還是24字節,因為c
無法作為內層結構體的尾填充。要想節省空間,你需要得新設計數據結構。
棘手的標量案例
只有在符號調試器能顯示枚舉類型的名稱而非原始整型數字時,使用枚舉來代替#define
才是個好辦法。然而,盡管枚舉必定與某種整型兼容,但C標準卻沒有指明究竟是何種底層整型。
請當心,重打包結構體時,枚舉型變量通常是int,這與編譯器相關;但也可能是short、long、甚至默認為char。編譯器可能會有progma預處理指令或命令行選項指定枚舉的尺寸。
long double
是個類似的故障點。有些C平臺以80位實現,有些是128位,還有些80位平臺將其填充到96或128位。
以上兩種情況,最好用sizeof()
來檢查存儲尺寸。
最后,在x86 Linux系統中,double有時會破自對齊規則的例;在結構體內,8字節的double可能只要求4字節對齊,而在結構體外,獨立的double變量又是8字節自對齊。這與編譯器和選項有關。
可讀性與緩存局部性
盡管按尺寸重排是最簡單的消除廢液的方式,卻不一定是正確的方式。還有兩個問題需要考量:可讀性與緩存局部性。
程序不僅與計算機交流,還與其他人交流。甚至(尤其是!)交流的對象只有將來你自己時,代碼可讀性依然重要。
笨拙地、機械地重排結構體可能有損可讀性。倘若有可能,最好這樣重排成員:將語義相關的數據放在一起,形成連貫的組。最理想的情況是,結構體的設計應與程序的設計相通。
當程序頻繁訪問某一結構體或其一部分時,若能將其放入一個緩存段,對提高性能頗有幫助。緩存段是這樣的內存塊——當處理器獲取內存中的任何單個地址時,會把整塊數據都取出來。 在64位x86上,一個緩存段為64字節,它開始于自對齊的地址。其他平臺通常為32字節。
為保持可讀性所做的工作(將相關和同時訪問的數據放在臨近位置)也會提高緩存段的局部性。這些都是需要明智地重排,并對數據的存取模式了然于心的原因。
如果代碼從多個線程并發訪問同一結構體,還存在第三個問題:緩存段彈跳(cache line bouncing)。為了盡量減少昂貴的總線通信,應當這樣安排數據——在一個更緊湊的循環里,從一個緩存段中讀數據,而向另一個寫入數據。
是的,某些時候,這種做法與前文將相關數據放入與緩存段長度相同塊的做法矛盾。多線程的確是個難題。緩存段彈跳和其他多線程優化問題是很高級的話題,值得單獨為它們寫份指導。這里我所能做的,只是讓你了解有這些問題存在。
其他打包技術
在為結構體瘦身時,重排序與其他技術結合在一起效果最好。例如結構體中有幾個布爾標志,可以考慮將其壓縮成1位的位域,然后把它們打包放在原本可能成為廢液的地方。
你可能會有一點兒存取時間的損失,但只要將工作集合壓縮得足夠小,那點損失可以靠避免緩存未命中補償。
更通用的原則是,選擇能把數據類型縮短的方法。以cvs-fast-export為例,我使用的一個壓縮方法是:利用RCS和CVS在1982年前還不存在這個事實,我棄用了64位的Unixtime_t
(在1970年開始為零),轉而用了一個32位的、從1982-01-01T00:00:00開始的偏移量;這樣日期會覆蓋到2118年。(注意:若使用這類技巧,要用邊界條件檢查以防討厭的Bug!)
這不僅減小了結構體的可見尺寸,還可以消除廢液和/或創造額外的機會來進行重新排序。這種良性串連的效果不難被觸發。
最冒險的打包方法是使用union。假如你知道結構體中的某些域永遠不會跟另一些域共同使用,可以考慮用union共享它們存儲空間。不過請特別小心并用回歸測試驗證。因為如果分析出現一丁點兒錯誤,就會引發從程序崩潰到微妙數據損壞(這種情況糟得多)間的各種錯誤。
工具
clang編譯器有個Wpadded選項,可以生成有關對齊和填充的信息。
還有個叫pahole的工具,我自己沒用過,但據說口碑很好。該工具與編譯器協同工作,生成關于結構體填充、對齊和緩存段邊界報告。
證明和例外
讀者可以下載一段程序源代碼packtest.c,驗證上文有關標量和結構體尺寸的結論。
如果你仔細檢查各種編譯器、選項和罕見硬件的稀奇組合,會發現我前面提到的部分規則存在例外。越早期的處理器設計例外越常見。
理解這些規則的第二個層次是,知其何時及如何會被打破。在我學習它們的日子里(1980年代早期),我們把不理解這些規則的人稱為“所有機器都是VAX綜合癥”的犧牲品。記住,世上所有電腦并非都是PC。
2、爭議分析
#include <stdio.h>
#include <stdbool.h>
struct foo1 {
char *p;
char c;
long x;
};
struct foo2 {
char c; /* 1 byte */
char pad[7]; /* 7 bytes */
char *p; /* 8 bytes */
long x; /* 8 bytes */
};
struct foo3 {
char *p; /* 8 bytes */
char c; /* 1 byte */
};
struct foo4 {
short s; /* 2 bytes */
char c; /* 1 byte */
};
struct foo5 {
char c;
struct foo5_inner {
char *p;
short x;
} inner;
};
struct foo6 {
short s;
char c;
int flip:1;
int nybble:4;
int septet:7;
};
struct foo7 {
int bigfield:31;
int littlefield:1;
};
struct foo8 {
int bigfield1:31;
int littlefield1:1;
int bigfield2:31;
int littlefield2:1;
};
struct foo9 {
int bigfield1:31;
int bigfield2:31;
int littlefield1:1;
int littlefield2:1;
};
struct foo10 {
char c;
struct foo10 *p;
short x;
};
struct foo11 {
struct foo11 *p;
short x;
char c;
};
struct foo12 {
struct foo12_inner {
char *p;
short x;
} inner;
char c;
};
main(int argc, char *argv)
{
printf("sizeof(char *) = %zu\n", sizeof(char *));
printf("sizeof(long) = %zu\n", sizeof(long));
printf("sizeof(int) = %zu\n", sizeof(int));
printf("sizeof(short) = %zu\n", sizeof(short));
printf("sizeof(char) = %zu\n", sizeof(char));
printf("sizeof(float) = %zu\n", sizeof(float));
printf("sizeof(double) = %zu\n", sizeof(double));
printf("sizeof(struct foo1) = %zu\n", sizeof(struct foo1));
printf("sizeof(struct foo2) = %zu\n", sizeof(struct foo2));
printf("sizeof(struct foo3) = %zu\n", sizeof(struct foo3));
printf("sizeof(struct foo4) = %zu\n", sizeof(struct foo4));
printf("sizeof(struct foo5) = %zu\n", sizeof(struct foo5));
printf("sizeof(struct foo6) = %zu\n", sizeof(struct foo6));
printf("sizeof(struct foo7) = %zu\n", sizeof(struct foo7));
printf("sizeof(struct foo8) = %zu\n", sizeof(struct foo8));
printf("sizeof(struct foo9) = %zu\n", sizeof(struct foo9));
printf("sizeof(struct foo10) = %zu\n", sizeof(struct foo10));
printf("sizeof(struct foo11) = %zu\n", sizeof(struct foo11));
printf("sizeof(struct foo12) = %zu\n", sizeof(struct foo12));
}
結果:foo6:
struct foo6 {
short s;
char c;
int flip:1;
int nybble:4;
int septet:7;
};
輸出:
sizeof(char *) = 8
sizeof(long) = 8
sizeof(int) = 4
sizeof(short) = 2
sizeof(char) = 1
sizeof(float) = 4
sizeof(double) = 8
sizeof(struct foo1) = 24
sizeof(struct foo2) = 24
sizeof(struct foo3) = 16
sizeof(struct foo4) = 4
sizeof(struct foo5) = 24
sizeof(struct foo6) = 8
sizeof(struct foo7) = 4
sizeof(struct foo8) = 8
sizeof(struct foo9) = 12
sizeof(struct foo10) = 24
sizeof(struct foo11) = 16
sizeof(struct foo12) = 24
sandbox> exited with status 0
The thing to know about bitfields is that they are implemented with word- and byte-level mask and rotate instructions operating on machine words, and cannot cross word boundaries.
C99 guarentees that bit-fields will be packed as tightly as possible, provided they don’t cross storage unit boundaries (6.7.2.1 #10).
This restriction is relaxed in C11 (6.7.2.1p11) and C++14 ([class.bit]p1); these revisions do not actually require struct foo9 to be 64 bits instead of 32;
a bit-field can span multiple allocation units instead of starting a new one. It’s up to the implementation to decide; GCC leaves it up to the ABI, which for x64 does prevent them from sharing an allocation unit.
在32bit機器內是這么填充的:
struct foo6 {
short s; /* 2 bytes */
char c; /* 1 byte */
int flip:1; /* total 1 bit */
int nybble:4; /* total 5 bits */
int pad1:3; /* pad to an 8-bit boundary */
int septet:7; /* 7 bits */
char pad2:25; /* pad to 32 bits */
};
實測卻是8byte。
為什么這里最后的7bit要重開一個word呢?為什么直接2byte就好了(因為最大的就是short=2byte而已嘛),答案在上面英文字里面的加粗部分說明了,也就是我們補充的時候,它不能跨越word,之前的bit1+bit4+pad3=1byte剛好踩到1word的邊界了,于是剩下的就必須按重開一個word,并按word對齊了。
對比驗證實驗1
之前認為是剛好踩著word的邊界了,所以值填充了4byte,那么我們把char c給去掉,也就是說前面只有2byte,那么生下來的12bit完全在2byte之內就可解決,從而總體少于4byte,因此沒有1word越界,就不需要word擴展,因此這里的結果因該是4byte。
#include <stdio.h>
struct foo6 {
short s; /* 2 bytes */
//char c; /* 1 byte */
int flip:1; /* total 1 bit */
int nybble:4; /* total 5 bits */
// int pad1:3; /* pad to an 8-bit boundary */
int septet:7; /* 7 bits */
// char pad2:25; /* pad to 32 bits */
};
int main () {
printf("sizeof(struct foo6) = %zu\n", sizeof(struct foo6));
return 0;
}
結果:
sizeof(struct foo6) = 4
完美!
對比實驗2
#include <stdio.h>
struct foo6 {
//short s; /* 2 bytes */
char c; /* 1 byte */
int flip:1; /* total 1 bit */
int nybble:4; /* total 5 bits */
// int pad1:3; /* pad to an 8-bit boundary */
int septet:7; /* 7 bits */
// char pad2:25; /* pad to 32 bits */
};
int main () {
printf("sizeof(struct foo6) = %zu\n", sizeof(struct foo6));
return 0;
}
輸出:
sizeof(struct foo6) = 4
這里為什么不是3呢?
因為對比實驗1里面開始的是short s,長度是2byte,因此后面的位域就向這個長度對齊;
而這里最長的就是char了,也就是1byte,因此位域向1byte對齊,因此最終就是4byte。
問題:這里最后的那個位septet假如不是7bit(< 1byte),而是大于1byte呢?這個該怎么對齊?
對照實驗3
struct foo6 {
//short s; /* 2 bytes */
char c; /* 1 byte */
int flip:1; /* total 1 bit */
int nybble:4; /* total 5 bits */
// int pad1:3; /* pad to an 8-bit boundary */
int septet:20; /* 7 bits */
// char pad2:25; /* pad to 32 bits */
};
最后的septet小于20bit,則占用空間是4byte,因為編譯器把超出1byte的bit往前面的兩個byte里面擠壓,使得空間盡量充滿,此時剛好24bit=3byte;
但是一旦septet=20bit,此時位域已經超了1bit,同時是跨word了,因此后面得填充4byte,因此最終的空間占用就是8byte。
對照實驗4
struct foo6 {
//short s; /* 2 bytes */
char c; /* 1 byte */
int flip:1; /* total 1 bit */
// int nybble:4; /* total 5 bits */
// int pad1:3; /* pad to an 8-bit boundary */
//int septet:20; /* 7 bits */
// char pad2:25; /* pad to 32 bits */
};
假如里面的bit域全部屏蔽掉,留下一個char c,那么空間占用就是1;
只要加入了bit位域,比如flip=1,應該結構聽總共2byte就足夠了,但是實際占用的字節是4byte,也就是說位域的最小單位是4byte。
對照實驗5
struct foo6 {
short s; /* 2 bytes */
char c; /* 1 byte */
//int flip:1; /* total 1 bit */
// int nybble:4; /* total 5 bits */
// int pad1:3; /* pad to an 8-bit boundary */
//int septet:20; /* 7 bits */
// char pad2:25; /* pad to 32 bits */
};
只留下一個short(2byte)跟一個char(1byte),**由于這個結構體內成員最大的長度是2byte,因此總的長度必須能被2整除,現在總長度只有3,因此肯定得補齊咯。