三分鐘讀懂編程語言C內存地址對齊與struct大小判斷

在筆試時,經常會遇到結構體大小的問題,實際就是在考內存地址對齊。在實際開發中,如果一個結構體會在內存中高頻地分配創建,那麼掌握內存地址對齊規則,通過簡單地自定義對齊方式,或者調整結構體成員的順序,可以有效地減少內存使用。另外,一些不用邊界對齊、可以在任何地址(包括奇數地址)引用任何數據類型的的機器,不在本文討論範圍之內。

什麼是地址對齊

計算機讀取或者寫入存儲器地址時,一般以字(因系統而異,32位系統為4個位元組)大小(N)的塊來執行操作。數據對齊就是將數據存儲區的首地址對齊字大小(N)的某個整數倍地址。為了對齊數據,有時需要在物理上相鄰的兩個數據之間保留或者插入一些無意義的位元組。內存對齊本事編譯器考慮是事情,但在C、C++語言中,可以人為修改對齊方式。如果你想加入我們跟我們一起學習的話可以加C/C++從入門到大牛 Ⅱ369203660 我們一起共同進步和學習!

Advertisements

為什麼要地址對齊

計算機會保證存儲器字的大小,至少要大於等於計算機支持的最大原始數據類型的大小。

這樣,一個原始數據類型就一定可以存放在一個存儲器字中,如果保證了數據是地址對齊的,那麼訪問一個原始數據就可以保證只訪問一個存儲器字,這有利於提高效率。如下圖

反之,如果一個數據不是按字大小內存對齊的(也就是最高位元組與最低位元組落在兩個字中),那麼,這個數據很可能落在兩個存儲器字中。如下圖

這時,計算機必須將數據訪問分割成多個存儲器字訪問,這需要更多複雜的操作。甚至,當這兩個字都不存在一個存儲器頁中是,處理器還必須在執行指令之前驗證兩個頁面是否存在,否則可能會發生未命中錯誤。另外,對一個存儲器字的操作是原子的,如果拆分成兩次訪問,也可能引發一些併發問題,比如從兩個字讀出來的數據段拼起來可能不是真實的數據,因為有另外的設備在寫。

Advertisements

起始地址約束(對齊係數)

C++11 引入 alignof 運算符,該運算符返回指定類型的對齊係數(以位元組為單位),其中宏__alignof在linuxgcc或者windows都有定義。

下面一段程序取幾個常用的基本數據類型。

C/C++從入門到大牛 Ⅱ369203660

1 #include <stdio.h> 2 #include <stdlib.h> 3 int main(){ 4 printf("char: %d\n",__alignof(char)); 5 printf("short: %d\n",__alignof(short)); 6 printf("int: %d\n",__alignof(int)); 7 printf("long: %d\n",__alignof(long)); 8 printf("double: %d\n",__alignof(double)); 9 return 0;10 }

分別在linux和windows下編譯運行,得到如下結果

類型LinuxWindows
char11
short22
int44
long84
double88

可以看到Linux下與Windows下,long類型對齊係數不一樣。並且對齊係數與類型自身所佔的大小也基本一致。

地址對齊對struct大小的影響

地址對齊主要影響到一些複雜的數據結構,比如struct結構體,因為有了內存地址對齊,大多數的struct實際佔用的大小顯得有些詭異。(注意,一個結構體的大小很可能超過存儲器字大小,這時跨字讀取數據已不可避免。但結構體本身及其成員還是需要繼續遵守對齊規則)

拿一個很簡單的結構體align1為例

C/C++從入門到大牛 Ⅱ369203660

1 struct align12 {3 char a;4 int b;5 char c;6 } sim[2];

如果不考慮任何對齊問題,只考慮結構體中每個成員應該佔用的大小,很顯然每個結構align1定義的變數是1(char)+4(int)+1(char)共6個位元組。但是實際上(至少在windows上)它佔用了12個位元組,原因就在於它有按照一定的規則進行內存地址對齊。下面是筆者參考各方面資料總結的四點結構體邊界對齊需滿足的要點:如果你想加入我們跟我們一起學習的話可以加C/C++從入門到大牛 Ⅱ369203660 我們一起共同進步和學習!

  1. 結構體變數本身的起始位置,必須是結構成員中對邊界要求最嚴格(對齊係數最大)的數據類型所要求的位置

  1. 比如double類型的起始地址約束(對齊係數)為8,那如果一個結構體包含double類型,則結構體變數本身的起始地址要能被8整除

  1. 成員必須考慮起始地址約束(對齊係數)和本身的大小,在windows和linux下,都可以使用__alignof(type)來查看type類型(原始基本類型)的起始地址約束(對齊係數)。

  2. 如果成員也是struct union之類的類型,則整體要照顧到部分,整體要滿足成員能符合起始地址約束

  3. 結構體可能需要在其所有成員之後填充一些位元組,以保證在分配結構體數組之後,每個數組元素要滿足起始地址約束。

讓我們再來仔細研究下結構體 align1定義的實例數組 sim[2]。我們先約定:佔用即表示本身大小及其後的空餘空間。

按要點1,則sim[0]的起始地址必須能被4整除,假設這個其實地址是4n,其中成員a的起始地址也是sim[0]的起始地址(按要點2,因為a為char類型,對齊係數為1,放哪都可以),a佔用一個位元組。

按要點2,成員b的起始地址必須能被4整除,很顯然不能直接放在成員a的後面(起始地址是4n+1,不能被4整除),所以需要跳過3個位元組存放b,那麼成員a實際佔用了4個位元組(我們的約定)。

同理,成員c可以直接放在b成員後面(起始地址是(4(n+2)),而且肯定可以被1整除)。

至此,sim[0]已經佔用了9個位元組了,但按照要點4,因為數組是連續的,為了保證其後的數組成員sim[1]也符合首地址能被4整除,必須將sim[0]的空間先後延長3個位元組至(4(n+3))。所以sim[0]實際要佔用12個位元組。

當然一個結構體不能有兩個大小,哪怕其後不再放align1類型的變數,系統也要為這個變數分配最大的12個位元組空間。

用一個簡單的佔位符來表示存儲,可表示為

1 // --sim[0]---- ----sim[1]--2 // a---bbbbc--- a---bbbbc---

用圖片描述如圖(一個正方形表示一個位元組空間)

很顯然,這個結構體對空間利用率不高,有50%的空間浪費。通過調整成員定義的順序,完全可以優化空間利用。個人的經驗是,本身佔用空間大的(如double類型)應該盡量往前面放。下面我們將intb;調整到第一位定義

C/C++從入門到大牛 Ⅱ369203660

1 struct align22 {3 int b;4 char a;5 char c;6 } sim[2];

通過分析不難發現,新的結構佔用8個位元組的空間。如圖

C/C++從入門到大牛 Ⅱ369203660

空間利用率提高到75%。當一個結構體足夠複雜時,通過調整順序或者自定義對齊方式,壓縮帶來的空間是非常可觀的。雖然,隨著內存越做越大,一般情況下開發已經不需要考慮這種問題。但是在海量服務下,如何死摳性能和減少資源佔用依然是開發需要考慮的問題。就像現在單機幾十萬併發tcp連接已經不難做到,為什麼還是有很多人在研究C10M(單機千萬連接)。

下面的程序是基於以上四項要點做的測試,特別注意MyStruct7,因為其中的成員包含數組。至於成員包含union的就比較簡單了,一般可以直接把union用union中最大的成員替換考慮,另外注意考慮要點3。另外,在一個位段定義中使用非int、signed int 、或者unsigned int類型,位段定義將變成一個普通的結構體,對齊原則也就遵從結構體的對齊原則。

測試代碼

文中所用的windows為windows7 64位, gcc版本為:gcc version 5.1.0(tdm64-1);linux為CentOSLinux release 7.2.1511 (Core),gcc版本是gccversion 4.8.5 20150623 (Red Hat 4.8.5-11) (GCC)


請養成良好的閱讀習慣,看完如果覺得喜歡的話請關注轉發評論收藏一下 感謝!

Advertisements

你可能會喜歡