偷偷摘套内射激情视频,久久精品99国产国产精,中文字幕无线乱码人妻,中文在线中文a,性爽19p

失落的C語(yǔ)言結(jié)構(gòu)體封裝藝術(shù)

開(kāi)發(fā) 后端
本文是關(guān)于削減C語(yǔ)言程序內(nèi)存占用空間的一項(xiàng)技術(shù)——為了減小內(nèi)存大小而手工重新封裝C結(jié)構(gòu)體聲明。你需要C語(yǔ)言的基本知識(shí)來(lái)讀懂本文。

Eric S. Raymond

目錄

1. 誰(shuí)該閱讀這篇文章

2. 我為什么寫這篇文章

3.對(duì)齊要求

4.填充

5.結(jié)構(gòu)體對(duì)齊及填充

6.結(jié)構(gòu)體重排序

7.難以處理的標(biāo)量的情況

8.可讀性和緩存局部性

9.其他封裝的技術(shù)

10.工具

11.證明及例外

12.版本履歷

 1. 誰(shuí)該閱讀這篇文章

本文是關(guān)于削減C語(yǔ)言程序內(nèi)存占用空間的一項(xiàng)技術(shù)——為了減小內(nèi)存大小而手工重新封裝C結(jié)構(gòu)體聲明。你需要C語(yǔ)言的基本知識(shí)來(lái)讀懂本文。

如果你要為內(nèi)存有限制的嵌入式系統(tǒng)、或者操作系統(tǒng)內(nèi)核寫代碼,那么你需要懂這項(xiàng)技術(shù)。如果你在處理極大的應(yīng)用程序數(shù)據(jù)集,以至于你的程序常常達(dá)到內(nèi)存的界限時(shí),這項(xiàng)技術(shù)是有幫助的。在任何你真的真的需要關(guān)注將高速緩存行未命中降到最低的應(yīng)用程序里,懂得這項(xiàng)技術(shù)是很好的。

最后,理解該技術(shù)是一個(gè)通往其他深?yuàn)W的C語(yǔ)言話題的入口。直到你掌握了它,你才成為一個(gè)高端的C程序員。直到你可以自己寫出這篇文檔并且可以理智地評(píng)論它,你才成為一位C語(yǔ)言大師。

2. 我為什么寫這篇文章

本文之所以存在,是因?yàn)樵?013年底,我發(fā)現(xiàn)我自己在大量使用一項(xiàng)C語(yǔ)言的優(yōu)化技術(shù),我早在二十多年前就已經(jīng)學(xué)會(huì)了該技術(shù),不過(guò)在那之后并沒(méi)怎么使用過(guò)。

我需要減小一個(gè)程序的內(nèi)存占用空間,它用了幾千——有時(shí)是幾十萬(wàn)個(gè)——C結(jié)構(gòu)體的實(shí)例。這個(gè)程序是cvs-fast-export,而問(wèn)題在于處理巨大的代碼庫(kù)時(shí),它曾因內(nèi)存耗盡的錯(cuò)誤而瀕臨崩潰。

在這類情況下,有好些辦法能極大地減少內(nèi)存使用的,比如小心地重新安排結(jié)構(gòu)體成員的順序之類的。這可以獲得巨大的收益——在我的事例中,我能夠減掉大約40%的工作區(qū)大小,使得程序能夠在不崩潰的情況下處理大得多的代碼庫(kù)。

當(dāng)我解決這個(gè)問(wèn)題,并且回想我所做的工作時(shí),我開(kāi)始發(fā)現(xiàn),我在用的這個(gè)技術(shù)現(xiàn)今應(yīng)被忘了大半了。一個(gè)網(wǎng)絡(luò)調(diào)查確認(rèn),C程序員好像已經(jīng)不再談?wù)撛摷夹g(shù)了,至少在搜索引擎可以看到的地方不談?wù)摿恕S袔讉€(gè)維基百科條目觸及了這個(gè)話題,但是我發(fā)現(xiàn)沒(méi)人能全面涵蓋。

實(shí)際上這個(gè)現(xiàn)象也是有合理的理由的。計(jì)算機(jī)科學(xué)課程(應(yīng)當(dāng))引導(dǎo)人們避開(kāi)細(xì)節(jié)的優(yōu)化而去尋找更好的算法。機(jī)器資源價(jià)格的暴跌已經(jīng)使得壓榨內(nèi)存用量變 得不那么必要了。而且,想當(dāng)年,駭客們?cè)?jīng)學(xué)習(xí)如何使用該技術(shù),使得他們?cè)谀吧挠布軜?gòu)上撞墻了——現(xiàn)在已經(jīng)不太常見(jiàn)的經(jīng)歷。

但是這項(xiàng)技術(shù)仍然在重要的場(chǎng)合有價(jià)值, 并且只要內(nèi)存有限,就能永存。本文目的就是讓C程序員免于重新找尋這項(xiàng)技術(shù),而讓他們可以集中精力在更重要的事情上。

3. 對(duì)齊要求(Alignment Requirement)

要明白的第一件事是,在現(xiàn)代處理器上,你的C編譯器在內(nèi)存里對(duì)基本的C數(shù)據(jù)類型的存放方式是受約束的,為的是內(nèi)存訪問(wèn)更快。

在x86或者ARM處理器上,基本的C數(shù)據(jù)類型的儲(chǔ)存一般并不是起始于內(nèi)存中的任意字節(jié)地址。而是,每種類型,除了字符型以外,都有對(duì)齊要求;字符 可以起始于任何字節(jié)地址,但是2字節(jié)的短整型必須起始于一個(gè)偶數(shù)地址,4字節(jié)整型或者浮點(diǎn)型必須起始于被4整除的地址,以及8字節(jié)長(zhǎng)整型或者雙精度浮點(diǎn)型 必須起始于被8整除的地址。帶符號(hào)與不帶符號(hào)之間沒(méi)有差別。

這個(gè)的行話叫:在x86和ARM上,基本的C語(yǔ)言類型是自對(duì)齊(self-aligned)的。指針,無(wú)論是32位(4字節(jié))亦或是64位(8字節(jié))也都是自對(duì)齊的。

自對(duì)齊使得訪問(wèn)更快,因?yàn)樗沟靡粭l指令就完成對(duì)類型化數(shù)據(jù)的取和存操作。沒(méi)有對(duì)齊的約束,反過(guò)來(lái),代碼最終可能會(huì)不得不跨越機(jī)器字的邊界做兩次或更多次訪問(wèn)。字符是特殊的情況;無(wú)論在一個(gè)單機(jī)器字中的何處,存取的花費(fèi)都是一樣的。那就是為什么字符型沒(méi)有被建議對(duì)齊。

我說(shuō)“在現(xiàn)代的處理器上”是因?yàn)?,在一些舊的處理器上,強(qiáng)制讓你的C程序違反對(duì)齊約束(比方說(shuō),將一個(gè)奇數(shù)的地址轉(zhuǎn)換成一個(gè)整型指針,并試圖使用 它)不僅會(huì)使你的代碼慢下來(lái),還會(huì)造成非法指令的錯(cuò)誤。比如在Sun的SPARC芯片上就曾經(jīng)這么干。實(shí)際上,只要夠決心并在處理器上設(shè)定正確(e18) 的硬件標(biāo)志位,你仍然可以在x86上觸發(fā)此錯(cuò)誤。

此外,自對(duì)齊不是唯一的可能的規(guī)則。歷史上,一些處理器(特別是那些缺少移位暫存器的)有更強(qiáng)的限制性規(guī)則。如果你做嵌入式系統(tǒng),你也許會(huì)在跌倒在這些叢林陷阱中。注意,這是有可能的。

有時(shí)你可以通過(guò)編譯指示,強(qiáng)制讓你的編譯器不使用處理器正常的對(duì)齊規(guī)則,通常是#pragma pack。不要隨意使用,因?yàn)樗鼤?huì)導(dǎo)致產(chǎn)生開(kāi)銷更大、更慢的代碼。使用我在這里描述的技術(shù),通常你可以節(jié)省同樣或者幾乎同樣多的內(nèi)存。

#pragma pack的唯一好處是,如果你不得不將你的C語(yǔ)言數(shù)據(jù)分布精確匹配到某些位級(jí)別的硬件或協(xié)議的需求,比如一個(gè)內(nèi)存映射的硬件端口,要求違反正常的對(duì)齊才能奏效。如果你遇到那種情況,并且你還未理解我在這里寫的這一切,你會(huì)有大麻煩的,我只能祝你好運(yùn)了。

#p#

4. 填充(Padding)

現(xiàn)在我們來(lái)看一個(gè)簡(jiǎn)單變量在內(nèi)存里的分布的例子??紤]在C模塊的最頂上的以下一系列的變量聲明:

  1. char *p; 
  2. char c; 
  3. int x; 

如果你不知道任何關(guān)于數(shù)據(jù)對(duì)齊的事情,你可能會(huì)假設(shè)這3個(gè)變量在內(nèi)存里會(huì)占據(jù)一個(gè)連續(xù)字節(jié)空間。那也就是說(shuō),在一個(gè)32位機(jī)器上,指針的4字節(jié),之后緊接著1字節(jié)的字符型,且之后緊接著4字節(jié)的整型。在64位機(jī)器只在指針是8字節(jié)上會(huì)有所不同。

這里是實(shí)際發(fā)生的(在x86或ARM或其他任何有自對(duì)齊的處理器類型)。p的存儲(chǔ)地址始于一個(gè)自對(duì)齊的4字節(jié)或者8字節(jié)邊界,取決于機(jī)器的字長(zhǎng)。這是指針對(duì)齊——可能是最嚴(yán)格的情況。

緊跟著的是c的存儲(chǔ)地址。但是x的4字節(jié)對(duì)齊要求,在內(nèi)存分布上造成了一個(gè)間隙;變成了恰似第四個(gè)變量插在其中,像這樣:

  1. char *p;      /* 4 or 8 bytes */ 
  2. char c;       /* 1 byte */ 
  3. char pad[3];  /* 3 bytes */ 
  4. int x;        /* 4 bytes */ 

pad[3]字符數(shù)組表示了一個(gè)事實(shí),結(jié)構(gòu)體中有3字節(jié)的無(wú)用的空間。 老派的術(shù)語(yǔ)稱之為“slop(水坑)”。

比較如果x是2字節(jié)的短整型會(huì)發(fā)生什么:

  1. char *p; 
  2. char c; 
  3. short x; 

在那個(gè)情況下,實(shí)際的內(nèi)存分布會(huì)變成這樣:

  1. char *p;      /* 4 or 8 bytes */ 
  2. char c;       /* 1 byte */ 
  3. char pad[1];  /* 1 byte */ 
  4. short x;      /* 2 bytes */ 

另一方面,如果x是一個(gè)在64位機(jī)上的長(zhǎng)整型

  1. char *p; 
  2. char c; 
  3. long x; 

最終我們會(huì)得到:

  1. char *p;     /* 8 bytes */ 
  2. char c;      /* 1 byte 
  3. char pad[7]; /* 7 bytes */ 
  4. long x;      /* 8 bytes */ 

如果你已仔細(xì)看到這里,現(xiàn)在你可能會(huì)想到越短的變量聲明先聲明的情況:

  1. char c; 
  2. char *p; 
  3. int x; 

如果實(shí)際的內(nèi)存分布寫成這樣:

  1. char c; 
  2. char pad1[M]; 
  3. char *p; 
  4. char pad2[N]; 
  5. int x; 

我們可以說(shuō)出M和N的值嗎?

首先,在這個(gè)例子中,N是零。x的地址,緊接在p之后,是保證指針對(duì)齊的,肯定比整型對(duì)齊更嚴(yán)格的。

M的值不太能預(yù)測(cè)。如果編譯器恰巧把c映射到機(jī)器字的最后一個(gè)字節(jié),下一個(gè)字節(jié)(p的第一部分)會(huì)成為下一個(gè)機(jī)器字的第一個(gè)字節(jié),并且正常地指針對(duì)齊。M為零。

c更可能會(huì)被映射到機(jī)器字的第一個(gè)字節(jié)。在那個(gè)情況下,M會(huì)是以保證p指針對(duì)齊而填補(bǔ)的數(shù)——在32位機(jī)器上是3,64位機(jī)器上是7。

如果你想讓那些變量占用更少的空間,你可以通過(guò)交換原序列中的x和c來(lái)達(dá)到效果。

  1. char *p;     /* 8 bytes */ 
  2. long x;      /* 8 bytes */ 
  3. char c;      /* 1 byte 

通常,對(duì)于C程序里少數(shù)的簡(jiǎn)單變量,你可以通過(guò)調(diào)整聲明順序來(lái)壓縮掉極少幾個(gè)字節(jié)數(shù),不會(huì)有顯著的節(jié)約。但當(dāng)用于非標(biāo)量變量(nonscalar variables),尤其是結(jié)構(gòu)體時(shí),這項(xiàng)技術(shù)會(huì)變得更有趣。

在我們講到非標(biāo)量變量之前,讓我們講一下標(biāo)量數(shù)組。在一個(gè)有自對(duì)齊類型的平臺(tái)上,字符、短整型、整型、長(zhǎng)整型、指針數(shù)組沒(méi)有內(nèi)部填充。每個(gè)成員會(huì)自動(dòng)自對(duì)齊到上一個(gè)之后(譯者注:原文 self-aligned at the end of the next one 似有誤)。

在下一章,我們會(huì)看到對(duì)于結(jié)構(gòu)體數(shù)組,一樣的規(guī)則并不一定正確。

5. 結(jié)構(gòu)體的對(duì)齊和填充

總的來(lái)說(shuō),一個(gè)結(jié)構(gòu)體實(shí)例會(huì)按照它最寬的標(biāo)量成員對(duì)齊。編譯器這樣做,把它作為最簡(jiǎn)單的方式來(lái)保證所有成員是自對(duì)齊,為了快速訪問(wèn)的目的。

而且,在C語(yǔ)言里,結(jié)構(gòu)體的地址與它第一個(gè)成員的地址是相同的——沒(méi)有前置填充。注意:在C++里,看上去像結(jié)構(gòu)體的類可能不遵守這個(gè)規(guī)則!(遵不遵守依賴于基類和虛擬內(nèi)存函數(shù)如何實(shí)現(xiàn),而且因編譯器而不同。)

(當(dāng)你不能確定此類事情時(shí),ANSI C提供了一個(gè)offsetof()宏,能夠用來(lái)表示出結(jié)構(gòu)體成員的偏移量。)

考慮這個(gè)結(jié)構(gòu)體:

  1. struct foo1 { 
  2.     char *p; 
  3.     char c; 
  4.     long x; 
  5. }; 

#p#

假設(shè)一臺(tái)64位的機(jī)器,任何struct foo1的實(shí)例會(huì)按8字節(jié)對(duì)齊。其中的任何一個(gè)的內(nèi)存分布看上去無(wú)疑應(yīng)該像這樣:

  1. struct foo1 { 
  2.     char *p;     /* 8 bytes */ 
  3.     char c;      /* 1 byte 
  4.     char pad[7]; /* 7 bytes */ 
  5.     long x;      /* 8 bytes */ 
  6. }; 

的分布就恰好就像這些類型的變量是單獨(dú)聲明的。但是如果我們把c放在第一個(gè),這就不是了。

  1. struct foo2 { 
  2.     char c;      /* 1 byte */ 
  3.     char pad[7]; /* 7 bytes */ 
  4.     char *p;     /* 8 bytes */ 
  5.     long x;      /* 8 bytes */ 
  6. }; 

如果成員是單獨(dú)的變量,c可以起始于任何字節(jié)邊界,并且pad的大小會(huì)不同。但因?yàn)閟truct foo2有按其最寬成員進(jìn)行的指針對(duì)齊,那就不可能了?,F(xiàn)在c必須于指針對(duì)齊,之后7個(gè)字節(jié)的填充就被鎖定了。

現(xiàn)在讓我們來(lái)說(shuō)說(shuō)關(guān)于在結(jié)構(gòu)體成員的尾隨填充(trailing padding)。要解釋這個(gè),我需要介紹一個(gè)基本概念,我稱之為結(jié)構(gòu)體的跨步地址(stride address)。它是跟隨結(jié)構(gòu)體數(shù)據(jù)后的第一個(gè)地址,與結(jié)構(gòu)體擁有同樣對(duì)齊方式

結(jié)構(gòu)體尾隨填充的通常規(guī)則是這樣的:編譯器的行為就如把結(jié)構(gòu)體尾隨填充到它的跨步地址。這條規(guī)則決定了sizeof()的返回值。

考慮在64位的x86或ARM上的這個(gè)例子:

  1. struct foo3 { 
  2.     char *p;     /* 8 bytes */ 
  3.     char c;      /* 1 byte */ 
  4. }; 
  5.   
  6. struct foo3 singleton; 
  7. struct foo3 quad[4]; 

你可能會(huì)認(rèn)為,sizeof(struct foo3)應(yīng)該是9,但實(shí)際上是16??绮降刂肥牵?amp;p)[2]的地址。如此,在quad數(shù)組中,每個(gè)成員有尾隨填充的7字節(jié),因?yàn)槊總€(gè)跟隨的結(jié)構(gòu)體的第一個(gè)成員都要自對(duì)齊到8字節(jié)的邊界上。內(nèi)存分布就如結(jié)構(gòu)體像這樣聲明:

  1. struct foo3 { 
  2.     char *p;     /* 8 bytes */ 
  3.     char c;      /* 1 byte */ 
  4.     char pad[7]; 
  5. }; 

作為對(duì)照,考慮下面的例子:

  1. struct foo4 { 
  2.     short s;     /* 2 bytes */ 
  3.     char c;      /* 1 byte */ 
  4. }; 

因?yàn)?span style="color: #0000ff;">s只需對(duì)齊到2字節(jié), 跨步地址就只有c后面的一個(gè)字節(jié),struct foo4作為一個(gè)整體,只需要一個(gè)字節(jié)的尾隨填充。它會(huì)像這樣分布

  1. struct foo4 { 
  2.     short s;     /* 2 bytes */ 
  3.     char c;      /* 1 byte */ 
  4.     char pad[1]; 
  5. }; 

并且sizeof(struct foo4)會(huì)返回4。

現(xiàn)在讓我們考慮位域(bitfield)。它們是你能夠聲明比字符寬度還小的結(jié)構(gòu)體域,小到1位,像這樣:

  1. struct foo5 { 
  2.     short s; 
  3.     char c; 
  4.     int flip:1; 
  5.     int nybble:4; 
  6.     int septet:7; 
  7. }; 

關(guān)于位域需要知道的事情是,它們以字或字節(jié)級(jí)別的掩碼和移位指令來(lái)實(shí)現(xiàn)。從編譯器的觀點(diǎn)來(lái)看,struct foo5的位域看上去像2字節(jié),16位的字符數(shù)組里只有12位被使用。接著是填充,使得這個(gè)結(jié)構(gòu)體的字節(jié)長(zhǎng)度成為sizeof(short)的倍數(shù)即最長(zhǎng)成員的大小。

  1. struct foo5 { 
  2.     short s;       /* 2 bytes */ 
  3.     char c;        /* 1 byte */ 
  4.     int flip:1;    /* total 1 bit */ 
  5.     int nybble:4;  /* total 5 bits */ 
  6.     int septet:7;  /* total 12 bits */ 
  7.     int pad1:4;    /* total 16 bits = 2 bytes */ 
  8.     char pad2;     /* 1 byte */ 
  9. }; 

這里是最后一個(gè)重要的細(xì)節(jié):如果你的結(jié)構(gòu)體含有結(jié)構(gòu)體的成員,里面的結(jié)構(gòu)體也需要按最長(zhǎng)的標(biāo)量對(duì)齊。假設(shè)如果你寫成這樣:

  1. struct foo6 { 
  2.     char c; 
  3.     struct foo5 { 
  4.         char *p; 
  5.         short x; 
  6.     } inner; 
  7. }; 

這個(gè)結(jié)構(gòu)體給了我們一個(gè)啟示,重新封裝結(jié)構(gòu)體可能節(jié)省空間。24個(gè)字節(jié)中,有13個(gè)字節(jié)是用作填充的。超過(guò)50%的無(wú)用空間!

#p#

6. 結(jié)構(gòu)體重排序(reordering)

現(xiàn)在你知道如何以及為何編譯器要插入填充,在你的結(jié)構(gòu)體之中或者之后,我們要考察你可以做些什么來(lái)擠掉這些“水坑”。這就是結(jié)構(gòu)體封裝的藝術(shù)。

第一件需要注意的事情是,“水坑”僅發(fā)生于兩個(gè)地方。一個(gè)是大數(shù)據(jù)類型(有更嚴(yán)格的對(duì)齊要求)的存儲(chǔ)區(qū)域緊跟在一個(gè)較小的數(shù)據(jù)類型的存儲(chǔ)區(qū)域之后。另一個(gè)是結(jié)構(gòu)體自然結(jié)束于它的跨步地址之前,需要填充,以使下一個(gè)實(shí)例可以正確對(duì)齊。

消除“水坑”的最簡(jiǎn)單的方法是按對(duì)齊的降序來(lái)對(duì)結(jié)構(gòu)體成員重排序。就是說(shuō):所有指針對(duì)齊的子域在前面,因?yàn)樵?4位的機(jī)器上,它們會(huì)有8字節(jié)。接下來(lái)是4字節(jié)的整型;然后是2字節(jié)的短整型;然后是字符域。

因此,舉個(gè)例子,考慮這個(gè)簡(jiǎn)單的鏈表結(jié)構(gòu)體:

  1. struct foo7 { 
  2.     char c; 
  3.     struct foo7 *p; 
  4.     short x; 
  5. }; 

顯現(xiàn)出隱含的“水坑”,這樣:

  1. struct foo7 { 
  2.     char c;         /* 1 byte */ 
  3.     char pad1[7];   /* 7 bytes */ 
  4.     struct foo7 *p; /* 8 bytes */ 
  5.     short x;        /* 2 bytes */ 
  6.     char pad2[6];   /* 6 bytes */ 
  7. }; 

24個(gè)字節(jié)。如果我們按大小重新排序,我們得到:

  1. struct foo8 { 
  2.     struct foo8 *p; 
  3.     short x; 
  4.     char c; 
  5. }; 

考慮到自對(duì)齊,我們看到?jīng)]有數(shù)據(jù)域需要填充。這是因?yàn)橐粋€(gè)較長(zhǎng)的、有較嚴(yán)格對(duì)齊的域的跨步地址,對(duì)于較短的、較不嚴(yán)格對(duì)齊的域來(lái)說(shuō),總是合法對(duì)齊的起始地址。所有重封裝的結(jié)構(gòu)體實(shí)際上需要的只是尾隨填充:

  1. struct foo8 { 
  2.     struct foo8 *p; /* 8 bytes */ 
  3.     short x;        /* 2 bytes */ 
  4.     char c;         /* 1 byte */ 
  5.     char pad[5];    /* 5 bytes */ 
  6. }; 

我們重封裝的轉(zhuǎn)變把大小降到了16字節(jié)。這可能看上去沒(méi)什么,但是假設(shè)你有一個(gè)200k的這樣的鏈表呢?節(jié)省的空間累積起來(lái)就不小了。

注意重排序并不能保證節(jié)省空間。把這個(gè)技巧運(yùn)用到早先的例子,struct foo6,我們得到:

  1. struct foo9 { 
  2.     struct foo9_inner { 
  3.         char *p;      /* 8 bytes */ 
  4.         int x;        /* 4 bytes */ 
  5.     } inner; 
  6.     char c;           /* 1 byte*/ 
  7. }; 

把填充寫出來(lái),就是這樣

  1. struct foo9 { 
  2.     struct foo9_inner { 
  3.         char *p;      /* 8 bytes */ 
  4.         int x;        /* 4 bytes */ 
  5.         char pad[4];  /* 4 bytes */ 
  6.     } inner; 
  7.     char c;           /* 1 byte*/ 
  8.     char pad[7];      /* 7 bytes */ 
  9. }; 

它仍然是24字節(jié),因?yàn)?span style="color: #0000ff;">c不能轉(zhuǎn)換到內(nèi)部結(jié)構(gòu)體成員的尾隨填充。為了獲得節(jié)省空間的好處,你需要重新設(shè)計(jì)你的數(shù)據(jù)結(jié)構(gòu)。

自從發(fā)布了這篇指南的第一版,我就被問(wèn)到了,如果通過(guò)重排序來(lái)得到最少的“水坑”是如此簡(jiǎn)單,為什么C編譯器不自動(dòng)完成呢?答案是:C語(yǔ)言最初是被 設(shè)計(jì)用來(lái)寫操作系統(tǒng)和其他接近硬件的語(yǔ)言。自動(dòng)重排序會(huì)妨礙到系統(tǒng)程序員規(guī)劃結(jié)構(gòu)體,精確匹配字節(jié)和內(nèi)存映射設(shè)備控制塊的位級(jí)分布的能力。

 7. 難以處理的標(biāo)量的情況

使用枚舉類型而不是#defines是個(gè)好主意,因?yàn)榉?hào)調(diào)試器可以用那些符號(hào)并且可以顯示它們,而不是未處理的整數(shù)。但是,盡管枚舉要保證兼容整型類型,C標(biāo)準(zhǔn)沒(méi)有明確規(guī)定哪些潛在的整型類型會(huì)被使用。

注意,當(dāng)重新封裝你的結(jié)構(gòu)體時(shí),雖然枚舉類型變量通常是整型,但它依賴于編譯器;它們可能是短整型、長(zhǎng)整型、甚至是默認(rèn)的字符型。你的編譯器可能有一個(gè)編譯指示或者命令行選項(xiàng)來(lái)強(qiáng)制規(guī)定大小。

long double類型也是個(gè)相似的麻煩點(diǎn)。有的C平臺(tái)以80位實(shí)現(xiàn),有的是128, 還有的80位的平臺(tái)填充到96或128位。

在這兩種情況下,最好用sizeof()來(lái)檢查存儲(chǔ)大小。

最后,在x86下,Linux的雙精度類型有時(shí)是一個(gè)自對(duì)齊規(guī)則的特例;一個(gè)8字節(jié)的雙精度數(shù)據(jù)在一個(gè)結(jié)構(gòu)體內(nèi)可以只要求4字節(jié)對(duì)齊,雖然單獨(dú)的雙精度變量要求8字節(jié)的自對(duì)齊。這依賴于編譯器及其選項(xiàng)。

#p#

8. 可讀性和緩存局部性

盡管按大小重排序是消除“水坑”的最簡(jiǎn)單的方式,但它不是必定正確的。還有兩個(gè)問(wèn)題:可讀性和緩存局部性。

程序不只是與計(jì)算機(jī)的交流,還是與其他人的交流。代碼可讀性是重要的,即便(或者尤其是?。┙涣鞯牧硪环讲恢皇俏磥?lái)的你。

笨拙的、機(jī)械的結(jié)構(gòu)體重排序會(huì)損害可讀性??赡艿脑?,最好重排域,使得語(yǔ)義相關(guān)的數(shù)據(jù)段緊緊相連,能形成連貫的組群。理想情況下,你的結(jié)構(gòu)體設(shè)計(jì)應(yīng)該傳達(dá)到你的程序。

當(dāng)你的程序經(jīng)常訪問(wèn)一個(gè)結(jié)構(gòu)體,或者結(jié)構(gòu)體的一部分,如果訪問(wèn)常命中緩存行(當(dāng)被告知去讀取任何一個(gè)塊里單個(gè)地址時(shí),你的處理器讀取的整一塊內(nèi)存)有助于提高性能。在64位x86機(jī)上一條緩存行為64字節(jié),始于一個(gè)自對(duì)齊的地址;在其他平臺(tái)上經(jīng)常是32字節(jié)。

你應(yīng)該做的事情是保持可讀性——把相關(guān)的和同時(shí)訪問(wèn)的數(shù)據(jù)組合到毗鄰的區(qū)域——這也會(huì)提高緩存行的局部性。這都是用代碼的數(shù)據(jù)訪問(wèn)模式的意識(shí),聰明地重排序的原因。

如果你的代碼有多線程并發(fā)訪問(wèn)一個(gè)結(jié)構(gòu)體,就會(huì)有第三個(gè)問(wèn)題:緩存行反彈(cache line bouncing)。為了減少代價(jià)高昂的總線通信,你應(yīng)該組織你的數(shù)據(jù),使得在緊湊的循環(huán)中,從一條緩存行中讀取,而在另一條緩存行中寫。

是的,這與之前關(guān)于把相關(guān)數(shù)據(jù)組成同樣大小的緩存行塊的指南有些矛盾。多線程是困難的。緩存行反彈以及其它的多線程優(yōu)化問(wèn)題是十分高級(jí)的話題,需要整篇關(guān)于它們的教程。這里我能做的最好的就就是讓你意識(shí)到這些問(wèn)題的存在。

9. 其它封裝技術(shù)

當(dāng)重排序與其他技術(shù)結(jié)合讓你的結(jié)構(gòu)體瘦身時(shí)效果最好。如果你在一個(gè)結(jié)構(gòu)體里有若干布爾型標(biāo)志,舉個(gè)例子,可以考慮將它們減小到1位的位域,并且將它們封裝到結(jié)構(gòu)體里的一個(gè)本會(huì)成為“水坑”的地方。

為此,你會(huì)碰到些許訪問(wèn)時(shí)間上的不利——但是如果它把工作區(qū)擠壓得足夠小,這些不利會(huì)被避免緩存不命中的得益所掩蓋。

更普遍的,尋找縮小數(shù)據(jù)域大小的方式。比如在cvs-fast-export里,我用的一項(xiàng)壓縮技術(shù)里用到了在1982年之前RCS和CVS代碼庫(kù)還不存在的知識(shí)。我把64位的Unix time_t(1970年作為起始0日期)減少到32位的、從1982-01-01T00:00:00開(kāi)始的時(shí)間偏移量;這會(huì)覆蓋2118年前的日期。(注意:如果你要玩這樣的花招,每當(dāng)你要設(shè)定字段,你都要做邊界檢查以防討厭的錯(cuò)誤!)

每一個(gè)這樣被縮小的域不僅減少了你結(jié)構(gòu)體顯在的大小,還會(huì)消除“水坑”,且/或創(chuàng)建額外的機(jī)會(huì)來(lái)得到域重排序的好處。這些效果的良性疊加不難得到。

最有風(fēng)險(xiǎn)的封裝形式是使用聯(lián)合體。如果你知道你結(jié)構(gòu)體中特定的域永遠(yuǎn)不會(huì)被用于與其他特定域的組合,考慮使用聯(lián)合體使得它們共享存儲(chǔ)空間。但你要額 外小心,并且用回歸測(cè)試來(lái)驗(yàn)證你的工作,因?yàn)槿绻愕纳芷诜治黾词褂休p微差錯(cuò),你會(huì)得到各種程序漏洞,從程序崩潰到(更糟糕的)不易發(fā)覺(jué)的數(shù)據(jù)損壞。

10. 工具

C語(yǔ)言編譯器有個(gè)-Wpadded選項(xiàng),能使它產(chǎn)生關(guān)于對(duì)齊空洞和填充的消息。

雖然我自己還沒(méi)用過(guò),但是一些反饋者稱贊了一個(gè)叫pahole的程序。這個(gè)工具與編譯器合作,產(chǎn)生關(guān)于你的結(jié)構(gòu)體的報(bào)告,記述了填充、對(duì)齊及緩存行邊界。

11. 證明及例外

你可以下載一個(gè)小程序的代碼,此代碼用來(lái)展示了上述標(biāo)量和結(jié)構(gòu)體大小的論斷。就是packtest.c。

如果你瀏覽足夠多的編譯器、選項(xiàng)和不常見(jiàn)的硬件的奇怪組合,你會(huì)發(fā)現(xiàn)針對(duì)我講述的一些規(guī)則的特例。如果你回到越舊的處理器設(shè)計(jì),就會(huì)越常見(jiàn)。

比知道這些規(guī)則更進(jìn)一步,是知道如何以及何時(shí)這些規(guī)則會(huì)被打破。在我學(xué)習(xí)它們的那些年(1980年代早期),我們把不懂這些的人稱為“世界都是VAX綜合征”的受害者。記住世界上不只有PC。

12. 版本履歷

1.5 @ 2014-01-03

解釋了為什么不自動(dòng)做結(jié)構(gòu)體成員的重排序。

1.4 @ 2014-01-06
關(guān)于x86 Linux下雙精度的注意。
1.3 @ 2014-01-03

關(guān)于難以處理的標(biāo)量實(shí)例、可讀性和緩存局部性及工具的段落。

1.2 @ 2014-01-02

修正了一個(gè)錯(cuò)誤的地址計(jì)算。

1.1 @ 2014-01-01

解釋為什么對(duì)齊的訪問(wèn)會(huì)更快。提及offsetof。各種小修復(fù),包括packtest.c的下載鏈接。

1.0 @ 2014-01-01

初版

原文鏈接:http://www.catb.org/esr/structure-packing/

譯文鏈接:http://blog.jobbole.com/57822/

責(zé)任編輯:陳四芳 來(lái)源: 伯樂(lè)在線
相關(guān)推薦

2009-08-14 11:05:28

C#語(yǔ)言的結(jié)構(gòu)體

2022-08-19 14:38:52

C語(yǔ)言結(jié)構(gòu)體struct

2010-12-30 09:22:58

C語(yǔ)言 數(shù)組

2020-07-21 15:20:20

語(yǔ)言結(jié)構(gòu)體共用體

2014-04-01 10:11:33

C語(yǔ)言指針

2013-06-26 10:13:32

C語(yǔ)言結(jié)構(gòu)體結(jié)構(gòu)體偏移

2023-07-29 15:03:29

2021-04-20 09:00:48

Go 語(yǔ)言結(jié)構(gòu)體type

2022-09-30 15:03:09

C語(yǔ)言深拷貝淺拷貝

2009-08-13 11:18:50

C#結(jié)構(gòu)體

2009-08-13 14:46:03

C#結(jié)構(gòu)體定義

2009-08-13 13:29:04

C#結(jié)構(gòu)體使用

2023-10-10 13:58:00

C語(yǔ)言代碼結(jié)構(gòu)體

2009-08-13 14:24:44

C#結(jié)構(gòu)體構(gòu)造函數(shù)

2009-08-13 14:56:46

C#的結(jié)構(gòu)體使用

2011-04-11 13:00:08

C++結(jié)構(gòu)體枚舉

2024-02-27 09:39:07

C語(yǔ)言cJSON開(kāi)發(fā)

2023-11-12 23:14:05

函數(shù)C 語(yǔ)言

2009-08-27 16:18:47

C#類C#結(jié)構(gòu)體

2009-08-13 15:03:58

C#結(jié)構(gòu)體變量
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)