github上有大神翻譯了一篇內(nèi)存對齊的英文文獻(xiàn)伶氢,我復(fù)現(xiàn)了一下過程吭历;
發(fā)現(xiàn)其中有個地方有出入(strcut foo6{}),因此特地查了下文獻(xiàn)躲因,做了下修正,記錄如下忌傻。
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)核代碼秤标,就有必要了解這項(xiàng)技術(shù)。如果數(shù)據(jù)集巨大宙刘,應(yīng)用時常逼近內(nèi)存極限苍姜,這項(xiàng)技術(shù)會有所幫助。倘若你非常非常關(guān)心如何最大限度地減少處理器緩存段(cache-line)未命中情況的發(fā)生悬包,這項(xiàng)技術(shù)也有所裨益衙猪。
最后,理解這項(xiàng)技術(shù)是通往其他C語言艱深話題的門徑布近。若不掌握垫释,就算不上高級C程序員。當(dāng)你自己也能寫出這樣的文檔撑瞧,并且有能力明智地評價(jià)它之后棵譬,才稱得上C語言大師。
緣何寫作本文
2013年底预伺,我【當(dāng)然指的是原作大觸懊4!】大量應(yīng)用了一項(xiàng)C語言優(yōu)化技術(shù)扭屁,這項(xiàng)技術(shù)是我早在二十余年前就已掌握的算谈,但彼時之后,鮮有使用料滥。
我需要減少一個程序?qū)?nèi)存空間的占用然眼,它使用了上千(有時甚至幾十萬)C結(jié)構(gòu)體實(shí)例。這個程序是cvs-fast-export葵腹,在將其應(yīng)用于大規(guī)模軟件倉庫時高每,程序會出現(xiàn)內(nèi)存耗盡錯誤屿岂。
通過精心調(diào)整結(jié)構(gòu)成體員的順序,可以在這種情況下大幅減少內(nèi)存占用鲸匿。其效果顯著——在上述案例中爷怀,可以減少40%的內(nèi)存空間。程序應(yīng)用于更大的軟件倉庫带欢,也不會因內(nèi)存耗盡而崩潰运授。
但隨著工作展開,我意識到這項(xiàng)技術(shù)在近些年幾乎已被遺忘乔煞。Web搜索證實(shí)了我的想法吁朦,現(xiàn)今的C程序員們似乎已不再談?wù)撨@些話題,至少從搜索引擎中看不到渡贾。維基百科有些條目涉及這一主題逗宜,但未曾有人完整闡述。
事出有因空骚。計(jì)算機(jī)科學(xué)課程(正確地)引導(dǎo)人們遠(yuǎn)離微觀優(yōu)化纺讲,轉(zhuǎn)而尋求更理想的算法。計(jì)算成本一路走低囤屹,令壓榨內(nèi)存的必要性變得越來越低刻诊。舊日里,黑客們通過在陌生的硬件架構(gòu)中跌跌撞撞學(xué)習(xí)——如今已不多見牺丙。
然而這項(xiàng)技術(shù)在關(guān)鍵時刻仍頗具價(jià)值则涯,并且只要內(nèi)存容量有限,價(jià)值就始終存在冲簿。本文意在節(jié)省C程序員重新發(fā)掘這項(xiàng)技術(shù)所需的時間粟判,讓他們有精力關(guān)注更重要任務(wù)。
對齊要求
首先需要了解的是峦剔,對于現(xiàn)代處理器档礁,C編譯器在內(nèi)存中放置基本C數(shù)據(jù)類型的方式受到約束,以令內(nèi)存的訪問速度更快吝沫。
在x86或ARM處理器中呻澜,基本C數(shù)據(jù)類型通常并不存儲于內(nèi)存中的隨機(jī)字節(jié)地址。實(shí)際情況是惨险,除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é))也都是自對齊的而芥。
自對齊可令訪問速度更快律罢,因?yàn)樗欣谏蓡沃噶睿╯ingle-instruction)存取這些類型的數(shù)據(jù)。另一方面棍丐,如若沒有對齊約束误辑,可能最終不得不通過兩個或更多指令訪問跨越機(jī)器字邊界的數(shù)據(jù)。字符數(shù)據(jù)是種特殊情況骄酗,因其始終處在單一機(jī)器字中稀余,所以無論存取何處的字符數(shù)據(jù)悦冀,開銷都是一致的趋翻。這也就是它不需要對齊的原因。
我提到“現(xiàn)代處理器”盒蟆,是因?yàn)橛行├掀脚_強(qiáng)迫C程序違反對齊規(guī)則(例如踏烙,為int指針分配一個奇怪的地址并試圖使用它),不僅令速度減慢历等,還會導(dǎo)致非法指令錯誤讨惩。例如Sun SPARC芯片就有這種問題。事實(shí)上寒屯,如果你下定決心荐捻,并恰當(dāng)?shù)卦谔幚砥髦性O(shè)置標(biāo)志位(e18),在x86平臺上寡夹,也能引發(fā)這種錯誤处面。
另外,自對齊并非唯一規(guī)則菩掏』杲牵縱觀歷史,有些處理器智绸,由其是那些缺乏桶式移位器(Barrel shifter)的處理器限制更多野揪。如果你從事嵌入式系統(tǒng)領(lǐng)域編程,有可能掉進(jìn)這些潛伏于草叢之中的陷阱瞧栗。小心這種可能斯稳。
你還可以通過pragma指令(通常為#pragma pack
)強(qiáng)迫編譯器不采用處理器慣用的對齊規(guī)則。但請別隨意運(yùn)用這種方式迹恐,因?yàn)樗鼜?qiáng)制生成開銷更大平挑、速度更慢的代碼。通常,采用我在下文介紹的方式通熄,可以節(jié)省相同或相近的內(nèi)存唆涝。
使用#pragma pack的唯一理由是——假如你需讓C語言的數(shù)據(jù)分布,與某種位級別的硬件或協(xié)議完全匹配(例如內(nèi)存映射硬件端口)唇辨,而違反通用對齊規(guī)則又不可避免廊酣。如果你處于這種困境,且不了解我所講述的內(nèi)容赏枚,那你已深陷泥潭亡驰,祝君好運(yùn)。
填充
我們來看一個關(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é)汉操。
然而實(shí)際情況(在x86、ARM或其他采用自對齊類型的平臺上)如下蒙兰。存儲p需要自對齊的4或8字節(jié)空間磷瘤,這取決于機(jī)器字的大小。這是指針對齊——極其嚴(yán)格搜变。
c緊隨其后采缚,但接下來x的4字節(jié)對齊要求,將強(qiáng)制在分布中生成了一段空白痹雅,仿佛在這段代碼中插入了第四個變量仰担,如下所示。
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é)的空間被浪費(fèi)掉了摔蓝。老派術(shù)語將其稱之為“廢液(slop)”。
如果x為2字節(jié)short:
char *p;
char c;
short x;
在這個例子中愉耙,實(shí)際分布將會是:
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 */
若你一路仔細(xì)讀下來,現(xiàn)在可能會思索朴沿,何不首先聲明較短的變量猜谚?
char c;
char *p;
int x;
假如實(shí)際內(nèi)存分布可以寫成下面這樣:
char c;
char pad1[M];
char *p;
char pad2[N];
int x;
那M
與N
分別為幾何败砂?
首先,在此例中魏铅,N
將為0昌犹,x
的地址緊隨p
之后,能確保是與指針對齊的览芳,因?yàn)橹羔樀膶R要求總比int嚴(yán)格斜姥。
M
的值就不易預(yù)測了。編譯器若是恰好將c
映射為機(jī)器字的最后一個字節(jié)沧竟,那么下一個字節(jié)(p
的第一個字節(jié))將恰好由此開始铸敏,并恰好與指針對齊。這種情況下悟泵,M
將為0杈笔。
不過更有可能的情況是,c
將被映射為機(jī)器字的首字節(jié)糕非。于是乎M
將會用于填充蒙具,以使p
指針對齊——32位系統(tǒng)中為3字節(jié),64位系統(tǒng)中為7字節(jié)峰弹。
中間情況也有可能發(fā)生店量。M的值有可能在0到7之間(32位系統(tǒng)為0到3)芜果,因?yàn)閏har可以從機(jī)器字的任何位置起始鞠呈。
倘若你希望這些變量占用的空間更少,那么可以交換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)體實(shí)例以其最寬的標(biāo)量成員為基準(zhǔn)進(jìn)行對齊。編譯器之所以如此温数,是因?yàn)榇四舜_保所有成員自對齊绣硝,實(shí)現(xiàn)快速訪問最簡便的方法。
此外撑刺,在C語言中域那,結(jié)構(gòu)體的地址,與其第一個成員的地址一致——不存在頭填充(leading padding)猜煮。小心:在C++中次员,與結(jié)構(gòu)體相似的類,可能會打破這條規(guī)則M醮(是否真的如此淑蔚,要看基類和虛擬成員函數(shù)是如何實(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
的實(shí)例都采用8字節(jié)對齊。不出所料囱桨,其內(nèi)存分布將會像下面這樣:
struct foo1 {
char *p; /* 8 bytes */
char c; /* 1 byte */
char pad[7]; /* 7 bytes */
long x; /* 8 bytes */
};
看起來仿佛與這些類型的變量單獨(dú)聲明別無二致仓犬。但假如我們將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
的大小則不再固定翠语。因?yàn)?code>struct foo2的指針需要與其最寬的成員為基準(zhǔn)對齊叽躯,這變得不再可能。現(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)體進(jìn)行尾填充齐板,直至它的跨步地址吵瞻。這條法則決定了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,但實(shí)際是16橡羞。它的跨步地址是(&p)[2]
眯停。于是,在quad
數(shù)組中卿泽,每個成員都有7字節(jié)的尾填充莺债,因?yàn)橄聜€結(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 */
};
因?yàn)?code>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é))層面的掩碼和移位指令實(shí)現(xiàn)的券犁。從編譯器的角度來看,struct foo5
中的位域就像2字節(jié)汹碱、16位的字符數(shù)組粘衬,只用到了其中12位。為了使結(jié)構(gòu)體的長度是其最寬成員長度sizeof(short)
的整數(shù)倍比被,接下來進(jìn)行了填充色难。
==這里存疑==泼舱。
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 */
};
這是最后一個重要細(xì)節(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
強(qiáng)迫外層結(jié)構(gòu)體與內(nèi)層結(jié)構(gòu)體指針對齊一致娇昙。在64位系統(tǒng)中尺迂,實(shí)際的內(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個為填充,浪費(fèi)了超過50%的空間股毫!
結(jié)構(gòu)體成員重排
理解了編譯器在結(jié)構(gòu)體中間和尾部插入填充的原因與方式后膳音,我們來看看如何榨出這些廢液。此即結(jié)構(gòu)體打包的技藝铃诬。
首先注意祭陷,廢液只存在于兩處苍凛。其一是較大的數(shù)據(jù)類型(需要更嚴(yán)格的對齊)跟在較小的數(shù)據(jù)類型之后。其二是結(jié)構(gòu)體自然結(jié)束的位置在跨步地址之前兵志,這里需要填充醇蝴,以使下個結(jié)構(gòu)體能正確地對齊。
消除廢液最簡單的方式想罕,是按對齊值遞減重新對結(jié)構(gòu)體成員排序悠栓。即讓所有指針對齊成員排在最前面,因?yàn)樵?4位系統(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ù)域之間都不需填充茅信。因?yàn)橛休^嚴(yán)對齊要求(更長)成員的跨步地址對不太嚴(yán)對齊要求的(更短)成員來說盾舌,總是合法的對齊地址。重打包過的結(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é)省空間。將這項(xiàng)技術(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é)仍稀,因?yàn)?code>c無法作為內(nèi)層結(jié)構(gòu)體的尾填充。要想節(jié)省空間埂息,你需要得新設(shè)計(jì)數(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、甚至默認(rèn)為char蛀蜜。編譯器可能會有progma預(yù)處理指令或命令行選項(xiàng)指定枚舉的尺寸刻两。
long double
是個類似的故障點(diǎn)。有些C平臺以80位實(shí)現(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)體外悼潭,獨(dú)立的double變量又是8字節(jié)自對齊。這與編譯器和選項(xiàng)有關(guān)舞箍。
可讀性與緩存局部性
盡管按尺寸重排是最簡單的消除廢液的方式舰褪,卻不一定是正確的方式。還有兩個問題需要考量:可讀性與緩存局部性疏橄。
程序不僅與計(jì)算機(jī)交流占拍,還與其他人交流。甚至(尤其是I悠取)交流的對象只有將來你自己時晃酒,代碼可讀性依然重要。
笨拙地窄绒、機(jī)械地重排結(jié)構(gòu)體可能有損可讀性贝次。倘若有可能,最好這樣重排成員:將語義相關(guān)的數(shù)據(jù)放在一起颗祝,形成連貫的組浊闪。最理想的情況是,結(jié)構(gòu)體的設(shè)計(jì)應(yīng)與程序的設(shè)計(jì)相通螺戳。
當(dāng)程序頻繁訪問某一結(jié)構(gòu)體或其一部分時,若能將其放入一個緩存段折汞,對提高性能頗有幫助倔幼。緩存段是這樣的內(nèi)存塊——當(dāng)處理器獲取內(nèi)存中的任何單個地址時,會把整塊數(shù)據(jù)都取出來爽待∷鹜 在64位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ú)為它們寫份指導(dǎo)障癌。這里我所能做的,只是讓你了解有這些問題存在辩尊。
其他打包技術(shù)
在為結(jié)構(gòu)體瘦身時涛浙,重排序與其他技術(shù)結(jié)合在一起效果最好。例如結(jié)構(gòu)體中有幾個布爾標(biāo)志摄欲,可以考慮將其壓縮成1位的位域轿亮,然后把它們打包放在原本可能成為廢液的地方。
你可能會有一點(diǎn)兒存取時間的損失胸墙,但只要將工作集合壓縮得足夠小我注,那點(diǎn)損失可以靠避免緩存未命中補(bǔ)償。
更通用的原則是迟隅,選擇能把數(shù)據(jù)類型縮短的方法但骨。以cvs-fast-export為例,我使用的一個壓縮方法是:利用RCS和CVS在1982年前還不存在這個事實(shí)智袭,我棄用了64位的Unixtime_t
(在1970年開始為零)奔缠,轉(zhuǎn)而用了一個32位的、從1982-01-01T00:00:00開始的偏移量吼野;這樣日期會覆蓋到2118年校哎。(注意:若使用這類技巧,要用邊界條件檢查以防討厭的Bug!)
這不僅減小了結(jié)構(gòu)體的可見尺寸闷哆,還可以消除廢液和/或創(chuàng)造額外的機(jī)會來進(jìn)行重新排序腰奋。這種良性串連的效果不難被觸發(fā)。
最冒險(xiǎn)的打包方法是使用union抱怔。假如你知道結(jié)構(gòu)體中的某些域永遠(yuǎn)不會跟另一些域共同使用劣坊,可以考慮用union共享它們存儲空間。不過請?zhí)貏e小心并用回歸測試驗(yàn)證屈留。因?yàn)槿绻治龀霈F(xiàn)一丁點(diǎn)兒錯誤局冰,就會引發(fā)從程序崩潰到微妙數(shù)據(jù)損壞(這種情況糟得多)間的各種錯誤。
工具
clang編譯器有個Wpadded選項(xiàng)绕沈,可以生成有關(guān)對齊和填充的信息锐想。
還有個叫pahole的工具,我自己沒用過乍狐,但據(jù)說口碑很好赠摇。該工具與編譯器協(xié)同工作,生成關(guān)于結(jié)構(gòu)體填充浅蚪、對齊和緩存段邊界報(bào)告藕帜。
證明和例外
讀者可以下載一段程序源代碼packtest.c,驗(yàn)證上文有關(guān)標(biāo)量和結(jié)構(gòu)體尺寸的結(jié)論惜傲。
如果你仔細(xì)檢查各種編譯器洽故、選項(xiàng)和罕見硬件的稀奇組合,會發(fā)現(xiàn)我前面提到的部分規(guī)則存在例外盗誊。越早期的處理器設(shè)計(jì)例外越常見时甚。
理解這些規(guī)則的第二個層次是,知其何時及如何會被打破哈踱。在我學(xué)習(xí)它們的日子里(1980年代早期)荒适,我們把不理解這些規(guī)則的人稱為“所有機(jī)器都是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機(jī)器內(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 */
};
實(shí)測卻是8byte邪财。
為什么這里最后的7bit要重開一個word呢陕壹?為什么直接2byte就好了(因?yàn)樽畲蟮木褪莝hort=2byte而已嘛),答案在上面英文字里面的加粗部分說明了树埠,也就是我們補(bǔ)充的時候糠馆,它不能跨越word,之前的bit1+bit4+pad3=1byte剛好踩到1word的邊界了弥奸,于是剩下的就必須按重開一個word榨惠,并按word對齊了。
對比驗(yàn)證實(shí)驗(yàn)1
之前認(rèn)為是剛好踩著word的邊界了盛霎,所以值填充了4byte赠橙,那么我們把char c給去掉,也就是說前面只有2byte愤炸,那么生下來的12bit完全在2byte之內(nèi)就可解決期揪,從而總體少于4byte均践,因此沒有1word越界苍苞,就不需要word擴(kuò)展,因此這里的結(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
完美诞仓!
對比實(shí)驗(yàn)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呢缤苫?
因?yàn)閷Ρ葘?shí)驗(yàn)1里面開始的是short s,長度是2byte墅拭,因此后面的位域就向這個長度對齊活玲;
而這里最長的就是char了,也就是1byte谍婉,因此位域向1byte對齊舒憾,因此最終就是4byte。
問題:這里最后的那個位septet假如不是7bit(< 1byte),而是大于1byte呢穗熬?這個該怎么對齊镀迂?
對照實(shí)驗(yàn)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唤蔗,因?yàn)榫幾g器把超出1byte的bit往前面的兩個byte里面擠壓探遵,使得空間盡量充滿,此時剛好24bit=3byte妓柜;
但是一旦septet=20bit箱季,此時位域已經(jīng)超了1bit,同時是跨word了领虹,因此后面得填充4byte规哪,因此最終的空間占用就是8byte。
對照實(shí)驗(yàn)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就足夠了,但是實(shí)際占用的字節(jié)是4byte努酸,也就是說位域的最小單位是4byte服爷。
對照實(shí)驗(yàn)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心褐,因此肯定得補(bǔ)齊咯。