內存對齊討論[修正]

源網址[英文]

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;

MN分別為幾何?

首先,在此例中,N將為0,x的地址緊隨p之后,能確保是與指針對齊的,因為指針的對齊要求總比int嚴格。

M的值就不易預測了。編譯器若是恰好將c映射為機器字的最后一個字節,那么下一個字節(p的第一個字節)將恰好由此開始,并恰好與指針對齊。這種情況下,M將為0。

不過更有可能的情況是,c將被映射為機器字的首字節。于是乎M將會用于填充,以使p指針對齊——32位系統中為3字節,64位系統中為7字節。

中間情況也有可能發生。M的值有可能在0到7之間(32位系統為0到3),因為char可以從機器字的任何位置起始。

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

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,因此肯定得補齊咯。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,316評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,481評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,241評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,939評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,697評論 6 409
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,182評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,247評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,406評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,933評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,772評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,973評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,516評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,209評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,638評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,866評論 1 285
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,644評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,953評論 2 373

推薦閱讀更多精彩內容