C語言精要總結-內存地址對齊與struct大小判斷篇

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

什麼是地址對齊

計算機讀取或者寫入存儲器地址時,通常以字(因系統而異,32位系統爲4個字節)大小(N)的塊來執行操做。數據對齊就是將數據存儲區的首地址對齊字大小(N)的某個整數倍地址爲了對齊數據,有時須要在物理上相鄰的兩個數據之間保留或者插入一些無心義的字節。內存對齊本事編譯器考慮是事情,但在C、C++語言中,能夠人爲修改對齊方式。windows

爲何要地址對齊

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

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

 

反之,若是一個數據不是按字大小內存對齊的(也就是最高字節與最低字節落在兩個字中),那麼,這個數據極可能落在兩個存儲器字中。以下圖併發

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

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

C++11 引入 alignof 運算符,該運算符返回指定類型的對齊係數(以字節爲單位),其中宏__alignof在linux gcc或者windows都有定義。ide

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

 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下編譯運行,獲得以下結果性能

類型 Linux Windows
char 1 1
short 2 2
int 4 4
long 8 4
double 8 8

能夠看到Linux下與Windows下,long類型對齊係數不同。而且對齊係數與類型自身所佔的大小也基本一致。測試

地址對齊對struct大小的影響

地址對齊主要影響到一些複雜的數據結構,好比struct結構體,由於有了內存地址對齊,大多數的struct實際佔用的大小顯得有些詭異。(注意,一個結構體的大小極可能超過存儲器字大小,這時跨字讀取數據已不可避免。但結構體自己及其成員仍是須要繼續遵照對齊規則)

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

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

若是不考慮任何對齊問題,只考慮結構體中每一個成員應該佔用的大小,很顯然每一個結構align1定義的變量是1(char)+4(int)+1(char)共6個字節。可是實際上(至少在windows上)它佔用了12個字節,緣由就在於它有按照必定的規則進行內存地址對齊。下面是筆者參考各方面資料總結的四點結構體邊界對齊需知足的要點:

  1. 結構體變量自己的起始位置,必須是結構成員中對邊界要求最嚴格(對齊係數最大)的數據類型所要求的位置
    1. 好比double類型的起始地址約束(對齊係數)爲8 ,那若是一個結構體包含double類型,則結構體變量自己的起始地址要能被8整除
  2. 成員必須考慮起始地址約束(對齊係數)和自己的大小,在windows和linux下,均可以使用__alignof(type)來查看type類型(原始基本類型)的起始地址約束(對齊係數)。
  3. 若是成員也是struct union之類的類型,則總體要照顧到部分,總體要知足成員能符合起始地址約束
  4. 結構體可能須要在其全部成員以後填充一些字節,以保證在分配結構體數組以後,每一個數組元素要知足起始地址約束

讓咱們再來仔細研究下結構體 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類型)應該儘可能往前面放。下面咱們將int b;調整到第一位定義

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

經過分析不難發現,新的結構佔用8個字節的空間。如圖

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

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

 1 #include <stdio.h>
 2 #include <stdlib.h>
 3 #include <stddef.h>
 4 /************************************************************************/
 5 /* 這裏約定:佔用即表示自己大小及其後的空餘空間
 6 /************************************************************************/
 7 struct MyStruct1    // 起始地址能被8整除
 8 {
 9     char a;            // 8
10     double b;        // 8
11     float c;        // 4
12     int d;            // 4
13 } m1;                // 24
14 
15 struct MyStruct2    // 起始地址能被8整除
16 {
17     int a;            // 4
18     float b;        // 4
19     char c;            // 8 // 後面 double的起始地址要能被8 整除,因此c補齊8個字節
20     double d;        // 8        
21 } m2;                // 24
22 
23 struct MyStruct3    // 起始地址能被8整除
24 {
25     short a;        // 2
26     char b;            // 6 // 同理,後面的元素的起始地址要能被 8 整除,因此b只要佔用6
27     double c;        // 8
28     int d;            // 8 // 須要在其後填充一些字節,以保證在分配數組以後,每一個數組元素要知足起始地址約束
29 } m3;                // 24
30 
31 struct MyStruct4
32 {
33     char a;            // 2 // 能被4整除的地址 +2以後能被2整除,因此a只要補1個字節
34     short b;        // 2 
35     int c;            // 4
36 } m4;                // 8
37 
38 struct MyStruct5    // 起始地址能被8整除
39 {
40     double a;        // 8
41     float b;        // 4
42     int c;            // 4    
43     short d;        // 2
44     char e;            // 6 由於後面牢牢挨着的MyStruct5 變量(在分配數組的時候)起始地址也要能被8整除,因此這個結構體總的大小必須是8的整數倍
45 } m5;                // 24
46 
47 struct MyStruct6    // 除4對齊
48 {
49     short a;        // 2
50     char b;            // 2
51     long c;            // 4
52     short d;        // 4  // 保證數組後面的元素也符合規則 (結構體首地址能夠除4)
53 } m6;                // 12
54 
55 struct MyStruct7    // 4 對齊
56 {
57     int a;            // 4
58     char b;            // 2
59     short c;        // 2
60     char d[6];        // 8
61 } m7;                    // 16
62 
63 int main(){
64     printf("m1 size : %d\n",sizeof m1);
65     printf("m2 size : %d\n",sizeof m2);
66     printf("m3 size : %d\n",sizeof m3);
67     printf("m4 size : %d\n",sizeof m4);
68     printf("m5 size : %d\n",sizeof m5);
69     printf("m6 size : %d\n",sizeof m6);
70     printf("m7 size : %d\n",sizeof m7);
71 
72     // offsetof 函數用來計算成員離結構體首地址偏移的字節數
73     printf("MyStruct1 b offset : %d\n",offsetof(struct MyStruct1,b));    // b偏移8個字節,因此成員a佔用8個字節
74     printf("MyStruct2 d offset : %d\n",offsetof(struct MyStruct2,d));    // d偏移了16個字節 
75     printf("MyStruct3 c offset : %d\n",offsetof(struct MyStruct3,c));    // 偏移8
76     printf("MyStruct4 b offset : %d\n",offsetof(struct MyStruct4,b));    // 偏移2
77     printf("MyStruct5 e offset : %d\n",offsetof(struct MyStruct5,e));    // 偏移16
78     printf("MyStruct6 c offset : %d\n",offsetof(struct MyStruct6,c));    // 偏移4
79     printf("MyStruct7 c offset : %d\n",offsetof(struct MyStruct7,c));    // 偏移
80     system("pause");
81     return 0;
82 }
測試代碼

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

參考文章:https://en.wikipedia.org/wiki/Data_structure_alignment

相關文章
相關標籤/搜索