內(nèi)存對齊討論[修正]

源網(wǎng)址[英文]

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;

MN分別為幾何败砂?

首先,在此例中魏铅,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ī)器字的任何位置起始鞠呈。

倘若你希望這些變量占用的空間更少,那么可以交換xc的次序右钾。

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ǔ)齊咯。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末笼踩,一起剝皮案震驚了整個濱河市逗爹,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌嚎于,老刑警劉巖掘而,帶你破解...
    沈念sama閱讀 216,591評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異于购,居然都是意外死亡袍睡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,448評論 3 392
  • 文/潘曉璐 我一進(jìn)店門肋僧,熙熙樓的掌柜王于貴愁眉苦臉地迎上來斑胜,“玉大人,你說我怎么就攤上這事色瘩∥苯眩” “怎么了?”我有些...
    開封第一講書人閱讀 162,823評論 0 353
  • 文/不壞的土叔 我叫張陵居兆,是天一觀的道長覆山。 經(jīng)常有香客問我,道長泥栖,這世上最難降的妖魔是什么簇宽? 我笑而不...
    開封第一講書人閱讀 58,204評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮吧享,結(jié)果婚禮上魏割,老公的妹妹穿的比我還像新娘。我一直安慰自己钢颂,他們只是感情好钞它,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,228評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著殊鞭,像睡著了一般遭垛。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上操灿,一...
    開封第一講書人閱讀 51,190評論 1 299
  • 那天锯仪,我揣著相機(jī)與錄音,去河邊找鬼趾盐。 笑死庶喜,一個胖子當(dāng)著我的面吹牛小腊,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播久窟,決...
    沈念sama閱讀 40,078評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼秩冈,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了瘸羡?” 一聲冷哼從身側(cè)響起漩仙,我...
    開封第一講書人閱讀 38,923評論 0 274
  • 序言:老撾萬榮一對情侶失蹤搓茬,失蹤者是張志新(化名)和其女友劉穎犹赖,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體卷仑,經(jīng)...
    沈念sama閱讀 45,334評論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡峻村,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,550評論 2 333
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了锡凝。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片粘昨。...
    茶點(diǎn)故事閱讀 39,727評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖窜锯,靈堂內(nèi)的尸體忽然破棺而出张肾,到底是詐尸還是另有隱情,我是刑警寧澤锚扎,帶...
    沈念sama閱讀 35,428評論 5 343
  • 正文 年R本政府宣布吞瞪,位于F島的核電站,受9級特大地震影響驾孔,放射性物質(zhì)發(fā)生泄漏芍秆。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,022評論 3 326
  • 文/蒙蒙 一翠勉、第九天 我趴在偏房一處隱蔽的房頂上張望妖啥。 院中可真熱鬧,春花似錦对碌、人聲如沸荆虱。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,672評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽怀读。三九已至,卻和暖如春华坦,著一層夾襖步出監(jiān)牢的瞬間愿吹,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,826評論 1 269
  • 我被黑心中介騙來泰國打工惜姐, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留犁跪,地道東北人椿息。 一個月前我還...
    沈念sama閱讀 47,734評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像坷衍,于是被迫代替她去往敵國和親寝优。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,619評論 2 354

推薦閱讀更多精彩內(nèi)容