github上有大神翻譯了一篇內(nèi)存對齊的英文文獻,我復(fù)現(xiàn)了一下過程;
發(fā)現(xiàn)其中有個地方有出入(strcut foo6{}),因此特地查了下文獻,做了下修正,記錄如下。
1、原文
作者:Eric S. Raymond
原文鏈接:http://www.catb.org/esr/structure-packing/
誰應(yīng)閱讀本文
本文探討如何通過手工重新打包C結(jié)構(gòu)體聲明,來減小內(nèi)存空間占用。你需要掌握基本的C語言知識,以理解本文所講述的內(nèi)容。
如果你在內(nèi)存容量受限的嵌入式系統(tǒng)中寫程序,或者編寫操作系統(tǒng)內(nèi)核代碼,就有必要了解這項技術(shù)。如果數(shù)據(jù)集巨大,應(yīng)用時常逼近內(nèi)存極限,這項技術(shù)會有所幫助。倘若你非常非常關(guān)心如何最大限度地減少處理器緩存段(cache-line)未命中情況的發(fā)生,這項技術(shù)也有所裨益。
最后,理解這項技術(shù)是通往其他C語言艱深話題的門徑。若不掌握,就算不上高級C程序員。當(dāng)你自己也能寫出這樣的文檔,并且有能力明智地評價它之后,才稱得上C語言大師。
緣何寫作本文
2013年底,我【當(dāng)然指的是原作大觸??!】大量應(yīng)用了一項C語言優(yōu)化技術(shù),這項技術(shù)是我早在二十余年前就已掌握的,但彼時之后,鮮有使用。
我需要減少一個程序?qū)?nèi)存空間的占用,它使用了上千(有時甚至幾十萬)C結(jié)構(gòu)體實例。這個程序是cvs-fast-export,在將其應(yīng)用于大規(guī)模軟件倉庫時,程序會出現(xiàn)內(nèi)存耗盡錯誤。
通過精心調(diào)整結(jié)構(gòu)成體員的順序,可以在這種情況下大幅減少內(nèi)存占用。其效果顯著——在上述案例中,可以減少40%的內(nèi)存空間。程序應(yīng)用于更大的軟件倉庫,也不會因內(nèi)存耗盡而崩潰。
但隨著工作展開,我意識到這項技術(shù)在近些年幾乎已被遺忘。Web搜索證實了我的想法,現(xiàn)今的C程序員們似乎已不再談?wù)撨@些話題,至少從搜索引擎中看不到。維基百科有些條目涉及這一主題,但未曾有人完整闡述。
事出有因。計算機科學(xué)課程(正確地)引導(dǎo)人們遠離微觀優(yōu)化,轉(zhuǎn)而尋求更理想的算法。計算成本一路走低,令壓榨內(nèi)存的必要性變得越來越低。舊日里,黑客們通過在陌生的硬件架構(gòu)中跌跌撞撞學(xué)習(xí)——如今已不多見。
然而這項技術(shù)在關(guān)鍵時刻仍頗具價值,并且只要內(nèi)存容量有限,價值就始終存在。本文意在節(jié)省C程序員重新發(fā)掘這項技術(shù)所需的時間,讓他們有精力關(guān)注更重要任務(wù)。
對齊要求
首先需要了解的是,對于現(xiàn)代處理器,C編譯器在內(nèi)存中放置基本C數(shù)據(jù)類型的方式受到約束,以令內(nèi)存的訪問速度更快。
在x86或ARM處理器中,基本C數(shù)據(jù)類型通常并不存儲于內(nèi)存中的隨機字節(jié)地址。實際情況是,除char外,所有其他類型都有“對齊要求”:char可起始于任意字節(jié)地址,2字節(jié)的short必須從偶數(shù)字節(jié)地址開始,4字節(jié)的int或float必須從能被4整除的地址開始,8比特的long和double必須從能被8整除的地址開始。無論signed(有符號)還是unsigned(無符號)都不受影響。
用行話來說,x86和ARM上的基本C類型是“自對齊(self-aligned)”的。關(guān)于指針,無論32位(4字節(jié))還是64位(8字節(jié))也都是自對齊的。
自對齊可令訪問速度更快,因為它有利于生成單指令(single-instruction)存取這些類型的數(shù)據(jù)。另一方面,如若沒有對齊約束,可能最終不得不通過兩個或更多指令訪問跨越機器字邊界的數(shù)據(jù)。字符數(shù)據(jù)是種特殊情況,因其始終處在單一機器字中,所以無論存取何處的字符數(shù)據(jù),開銷都是一致的。這也就是它不需要對齊的原因。
我提到“現(xiàn)代處理器”,是因為有些老平臺強迫C程序違反對齊規(guī)則(例如,為int指針分配一個奇怪的地址并試圖使用它),不僅令速度減慢,還會導(dǎo)致非法指令錯誤。例如Sun SPARC芯片就有這種問題。事實上,如果你下定決心,并恰當(dāng)?shù)卦谔幚砥髦性O(shè)置標(biāo)志位(e18),在x86平臺上,也能引發(fā)這種錯誤。
另外,自對齊并非唯一規(guī)則。縱觀歷史,有些處理器,由其是那些缺乏桶式移位器(Barrel shifter)的處理器限制更多。如果你從事嵌入式系統(tǒng)領(lǐng)域編程,有可能掉進這些潛伏于草叢之中的陷阱。小心這種可能。
你還可以通過pragma指令(通常為#pragma pack)強迫編譯器不采用處理器慣用的對齊規(guī)則。但請別隨意運用這種方式,因為它強制生成開銷更大、速度更慢的代碼。通常,采用我在下文介紹的方式,可以節(jié)省相同或相近的內(nèi)存。
使用#pragma pack的唯一理由是——假如你需讓C語言的數(shù)據(jù)分布,與某種位級別的硬件或協(xié)議完全匹配(例如內(nèi)存映射硬件端口),而違反通用對齊規(guī)則又不可避免。如果你處于這種困境,且不了解我所講述的內(nèi)容,那你已深陷泥潭,祝君好運。
填充
我們來看一個關(guān)于變量在內(nèi)存中分布的簡單案例。思考形式如下的一系列變量聲明,它們處在一個C模塊的頂層。
char *p;
char c;
int x;
假如你對數(shù)據(jù)對齊一無所知,也許以為這3個變量將在內(nèi)存中占據(jù)一段連續(xù)空間。也就是說,在32位系統(tǒng)上,一個4字節(jié)指針之后緊跟著1字節(jié)的char,其后又緊跟著4字節(jié)int。在64位系統(tǒng)中,唯一的區(qū)別在于指針將占用8字節(jié)。
然而實際情況(在x86、ARM或其他采用自對齊類型的平臺上)如下。存儲p需要自對齊的4或8字節(jié)空間,這取決于機器字的大小。這是指針對齊——極其嚴格。
c緊隨其后,但接下來x的4字節(jié)對齊要求,將強制在分布中生成了一段空白,仿佛在這段代碼中插入了第四個變量,如下所示。
char *p; /* 4 or 8 bytes */
char c; /* 1 byte */
char pad[3]; /* 3 bytes */
int x; /* 4 bytes */
字符數(shù)組pad[3]意味著在這個結(jié)構(gòu)體中,有3個字節(jié)的空間被浪費掉了。老派術(shù)語將其稱之為“廢液(slop)”。
如果x為2字節(jié)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位系統(tǒng)中的long:
char *p;
char c;
long x;
我們將得到:
char *p; /* 8 bytes */
char c; /* 1 byte */
char pad[7]; /* 7 bytes */
long x; /* 8 bytes */
若你一路仔細讀下來,現(xiàn)在可能會思索,何不首先聲明較短的變量?
char c;
char *p;
int x;
假如實際內(nèi)存分布可以寫成下面這樣:
char c;
char pad1[M];
char *p;
char pad2[N];
int x;
那M與N分別為幾何?
首先,在此例中,N將為0,x的地址緊隨p之后,能確保是與指針對齊的,因為指針的對齊要求總比int嚴格。
M的值就不易預(yù)測了。編譯器若是恰好將c映射為機器字的最后一個字節(jié),那么下一個字節(jié)(p的第一個字節(jié))將恰好由此開始,并恰好與指針對齊。這種情況下,M將為0。
不過更有可能的情況是,c將被映射為機器字的首字節(jié)。于是乎M將會用于填充,以使p指針對齊——32位系統(tǒng)中為3字節(jié),64位系統(tǒng)中為7字節(jié)。
中間情況也有可能發(fā)生。M的值有可能在0到7之間(32位系統(tǒng)為0到3),因為char可以從機器字的任何位置起始。
倘若你希望這些變量占用的空間更少,那么可以交換x與c的次序。
char *p; /* 8 bytes */
long x; /* 8 bytes */
char c; /* 1 byte */
通常,對于C代碼中的少數(shù)標(biāo)量變量(scalar variable),采用調(diào)換聲明次序的方式能節(jié)省幾個有限的字節(jié),效果不算明顯。而將這種技術(shù)應(yīng)用于非標(biāo)量變量(nonscalar variable)——尤其是結(jié)構(gòu)體,則要有趣多了。
在講述這部分內(nèi)容前,我們先對標(biāo)量數(shù)組做個說明。在具有自對齊類型的平臺上,char、short、int、long和指針數(shù)組都沒有內(nèi)部填充,每個成員都與下一個成員自動對齊。
在下一節(jié)我們將會看到,這種情況對結(jié)構(gòu)體數(shù)組并不適用。
結(jié)構(gòu)體的對齊和填充
通常情況下,結(jié)構(gòu)體實例以其最寬的標(biāo)量成員為基準(zhǔn)進行對齊。編譯器之所以如此,是因為此乃確保所有成員自對齊,實現(xiàn)快速訪問最簡便的方法。
此外,在C語言中,結(jié)構(gòu)體的地址,與其第一個成員的地址一致——不存在頭填充(leading padding)。小心:在C++中,與結(jié)構(gòu)體相似的類,可能會打破這條規(guī)則?。ㄊ欠裾娴娜绱耍椿惡吞摂M成員函數(shù)是如何實現(xiàn)的,與不同的編譯器也有關(guān)聯(lián)。)
假如你對此有疑惑,ANSI C提供了一個offsetof()宏,可用于讀取結(jié)構(gòu)體成員位移。
考慮這個結(jié)構(gòu)體:
struct foo1 {
char *p;
char c;
long x;
};
假定處在64位系統(tǒng)中,任何struct fool的實例都采用8字節(jié)對齊。不出所料,其內(nèi)存分布將會像下面這樣:
struct foo1 {
char *p; /* 8 bytes */
char c; /* 1 byte */
char pad[7]; /* 7 bytes */
long x; /* 8 bytes */
};
看起來仿佛與這些類型的變量單獨聲明別無二致。但假如我們將c放在首位,就會發(fā)現(xiàn)情況并非如此。
struct foo2 {
char c; /* 1 byte */
char pad[7]; /* 7 bytes */
char *p; /* 8 bytes */
long x; /* 8 bytes */
};
如果成員是互不關(guān)聯(lián)的變量,c便可能從任意位置起始,pad的大小則不再固定。因為struct foo2的指針需要與其最寬的成員為基準(zhǔn)對齊,這變得不再可能?,F(xiàn)在c需要指針對齊,接下來填充的7個字節(jié)被鎖定了。
現(xiàn)在,我們來談?wù)劷Y(jié)構(gòu)體的尾填充(trailing padding)。為了解釋它,需要引入一個基本概念,我將其稱為結(jié)構(gòu)體的“跨步地址(stride address)”。它是在結(jié)構(gòu)體數(shù)據(jù)之后,與結(jié)構(gòu)體對齊一致的首個地址。
結(jié)構(gòu)體尾填充的通用法則是:編譯器將會對結(jié)構(gòu)體進行尾填充,直至它的跨步地址。這條法則決定了sizeof()的返回值。
考慮64位x86或ARM系統(tǒng)中的這個例子:
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數(shù)組中,每個成員都有7字節(jié)的尾填充,因為下個結(jié)構(gòu)體的首個成員需要在8字節(jié)邊界上對齊。內(nèi)存分布就好像這個結(jié)構(gòu)是這樣聲明的:
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字節(jié)對齊,跨步地址僅在c的1字節(jié)之后,整個struct foo4也只需要1字節(jié)的尾填充。形式如下:
struct foo4 {
short s; /* 2 bytes */
char c; /* 1 byte */
char pad[1];
};
sizeof(struct foo4)的返回值將為4。
現(xiàn)在我們考慮位域(bitfields)。利用位域,你能聲明比字符寬度更小的成員,低至1位,例如:
struct foo5 {
short s;
char c;
int flip:1;
int nybble:4;
int septet:7;
};
關(guān)于位域需要了解的是,它們是由字(或字節(jié))層面的掩碼和移位指令實現(xiàn)的。從編譯器的角度來看,struct foo5中的位域就像2字節(jié)、16位的字符數(shù)組,只用到了其中12位。為了使結(jié)構(gòu)體的長度是其最寬成員長度sizeof(short)的整數(shù)倍,接下來進行了填充。
==這里存疑==。
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 */
};
這是最后一個重要細節(jié):如果你的結(jié)構(gòu)體中含有結(jié)構(gòu)體成員,內(nèi)層結(jié)構(gòu)體也要和最長的標(biāo)量有相同的對齊。假如你寫下了這段代碼:
struct foo6 {
char c;
struct foo5 {
char *p;
short x;
} inner;
};
內(nèi)層結(jié)構(gòu)體成員char *p強迫外層結(jié)構(gòu)體與內(nèi)層結(jié)構(gòu)體指針對齊一致。在64位系統(tǒng)中,實際的內(nèi)存分布將類似這樣:
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;
};
它啟示我們,能通過重新打包節(jié)省空間。24個字節(jié)中,有13個為填充,浪費了超過50%的空間!
結(jié)構(gòu)體成員重排
理解了編譯器在結(jié)構(gòu)體中間和尾部插入填充的原因與方式后,我們來看看如何榨出這些廢液。此即結(jié)構(gòu)體打包的技藝。
首先注意,廢液只存在于兩處。其一是較大的數(shù)據(jù)類型(需要更嚴格的對齊)跟在較小的數(shù)據(jù)類型之后。其二是結(jié)構(gòu)體自然結(jié)束的位置在跨步地址之前,這里需要填充,以使下個結(jié)構(gòu)體能正確地對齊。
消除廢液最簡單的方式,是按對齊值遞減重新對結(jié)構(gòu)體成員排序。即讓所有指針對齊成員排在最前面,因為在64位系統(tǒng)中它們占用8字節(jié);然后是4字節(jié)的int;再然后是2字節(jié)的short,最后是字符。
因此,以簡單的鏈表結(jié)構(gòu)體為例:
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字節(jié)。如果按長度重排,我們得到:
struct foo8 {
struct foo8 *p;
short x;
char c;
};
考慮到自對齊,我們看到所有數(shù)據(jù)域之間都不需填充。因為有較嚴對齊要求(更長)成員的跨步地址對不太嚴對齊要求的(更短)成員來說,總是合法的對齊地址。重打包過的結(jié)構(gòu)體只需要尾填充:
struct foo8 {
struct foo8 *p; /* 8 bytes */
short x; /* 2 bytes */
char c; /* 1 byte */
char pad[5]; /* 5 bytes */
};
重新打包將空間降為16字節(jié)。也許看起來不算很多,但假如這個鏈表的長度有20萬呢?將會積少成多。
注意,重新打包不能確保在所有情況下都能節(jié)省空間。將這項技術(shù)應(yīng)用于更靠前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 */
};
結(jié)果還是24字節(jié),因為c無法作為內(nèi)層結(jié)構(gòu)體的尾填充。要想節(jié)省空間,你需要得新設(shè)計數(shù)據(jù)結(jié)構(gòu)。
棘手的標(biāo)量案例
只有在符號調(diào)試器能顯示枚舉類型的名稱而非原始整型數(shù)字時,使用枚舉來代替#define才是個好辦法。然而,盡管枚舉必定與某種整型兼容,但C標(biāo)準(zhǔn)卻沒有指明究竟是何種底層整型。
請當(dāng)心,重打包結(jié)構(gòu)體時,枚舉型變量通常是int,這與編譯器相關(guān);但也可能是short、long、甚至默認為char。編譯器可能會有progma預(yù)處理指令或命令行選項指定枚舉的尺寸。
long double是個類似的故障點。有些C平臺以80位實現(xiàn),有些是128位,還有些80位平臺將其填充到96或128位。
以上兩種情況,最好用sizeof()來檢查存儲尺寸。
最后,在x86 Linux系統(tǒng)中,double有時會破自對齊規(guī)則的例;在結(jié)構(gòu)體內(nèi),8字節(jié)的double可能只要求4字節(jié)對齊,而在結(jié)構(gòu)體外,獨立的double變量又是8字節(jié)自對齊。這與編譯器和選項有關(guān)。
可讀性與緩存局部性
盡管按尺寸重排是最簡單的消除廢液的方式,卻不一定是正確的方式。還有兩個問題需要考量:可讀性與緩存局部性。
程序不僅與計算機交流,還與其他人交流。甚至(尤其是?。┙涣鞯膶ο笾挥袑砟阕约簳r,代碼可讀性依然重要。
笨拙地、機械地重排結(jié)構(gòu)體可能有損可讀性。倘若有可能,最好這樣重排成員:將語義相關(guān)的數(shù)據(jù)放在一起,形成連貫的組。最理想的情況是,結(jié)構(gòu)體的設(shè)計應(yīng)與程序的設(shè)計相通。
當(dāng)程序頻繁訪問某一結(jié)構(gòu)體或其一部分時,若能將其放入一個緩存段,對提高性能頗有幫助。緩存段是這樣的內(nèi)存塊——當(dāng)處理器獲取內(nèi)存中的任何單個地址時,會把整塊數(shù)據(jù)都取出來?!≡?4位x86上,一個緩存段為64字節(jié),它開始于自對齊的地址。其他平臺通常為32字節(jié)。
為保持可讀性所做的工作(將相關(guān)和同時訪問的數(shù)據(jù)放在臨近位置)也會提高緩存段的局部性。這些都是需要明智地重排,并對數(shù)據(jù)的存取模式了然于心的原因。
如果代碼從多個線程并發(fā)訪問同一結(jié)構(gòu)體,還存在第三個問題:緩存段彈跳(cache line bouncing)。為了盡量減少昂貴的總線通信,應(yīng)當(dāng)這樣安排數(shù)據(jù)——在一個更緊湊的循環(huán)里,從一個緩存段中讀數(shù)據(jù),而向另一個寫入數(shù)據(jù)。
是的,某些時候,這種做法與前文將相關(guān)數(shù)據(jù)放入與緩存段長度相同塊的做法矛盾。多線程的確是個難題。緩存段彈跳和其他多線程優(yōu)化問題是很高級的話題,值得單獨為它們寫份指導(dǎo)。這里我所能做的,只是讓你了解有這些問題存在。
其他打包技術(shù)
在為結(jié)構(gòu)體瘦身時,重排序與其他技術(shù)結(jié)合在一起效果最好。例如結(jié)構(gòu)體中有幾個布爾標(biāo)志,可以考慮將其壓縮成1位的位域,然后把它們打包放在原本可能成為廢液的地方。
你可能會有一點兒存取時間的損失,但只要將工作集合壓縮得足夠小,那點損失可以靠避免緩存未命中補償。
更通用的原則是,選擇能把數(shù)據(jù)類型縮短的方法。以cvs-fast-export為例,我使用的一個壓縮方法是:利用RCS和CVS在1982年前還不存在這個事實,我棄用了64位的Unixtime_t(在1970年開始為零),轉(zhuǎn)而用了一個32位的、從1982-01-01T00:00:00開始的偏移量;這樣日期會覆蓋到2118年。(注意:若使用這類技巧,要用邊界條件檢查以防討厭的Bug?。?/p>
這不僅減小了結(jié)構(gòu)體的可見尺寸,還可以消除廢液和/或創(chuàng)造額外的機會來進行重新排序。這種良性串連的效果不難被觸發(fā)。
最冒險的打包方法是使用union。假如你知道結(jié)構(gòu)體中的某些域永遠不會跟另一些域共同使用,可以考慮用union共享它們存儲空間。不過請?zhí)貏e小心并用回歸測試驗證。因為如果分析出現(xiàn)一丁點兒錯誤,就會引發(fā)從程序崩潰到微妙數(shù)據(jù)損壞(這種情況糟得多)間的各種錯誤。
工具
clang編譯器有個Wpadded選項,可以生成有關(guān)對齊和填充的信息。
還有個叫pahole的工具,我自己沒用過,但據(jù)說口碑很好。該工具與編譯器協(xié)同工作,生成關(guān)于結(jié)構(gòu)體填充、對齊和緩存段邊界報告。
證明和例外
讀者可以下載一段程序源代碼packtest.c,驗證上文有關(guān)標(biāo)量和結(jié)構(gòu)體尺寸的結(jié)論。
如果你仔細檢查各種編譯器、選項和罕見硬件的稀奇組合,會發(fā)現(xiàn)我前面提到的部分規(guī)則存在例外。越早期的處理器設(shè)計例外越常見。
理解這些規(guī)則的第二個層次是,知其何時及如何會被打破。在我學(xué)習(xí)它們的日子里(1980年代早期),我們把不理解這些規(guī)則的人稱為“所有機器都是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));
}
結(jié)果: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機器內(nèi)是這么填充的:
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之內(nèi)就可解決,從而總體少于4byte,因此沒有1word越界,就不需要word擴展,因此這里的結(jié)果因該是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;
}
結(jié)果:
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,此時位域已經(jīng)超了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,應(yīng)該結(jié)構(gòu)聽總共2byte就足夠了,但是實際占用的字節(jié)是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),**由于這個結(jié)構(gòu)體內(nèi)成員最大的長度是2byte,因此總的長度必須能被2整除,現(xiàn)在總長度只有3,因此肯定得補齊咯。