1. 程式人生 > >C語言精要總結-內存地址對齊與struct大小判斷篇

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

超過 等於 合規 占用 編譯 約定 long 並發 分享

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

什麽是地址對齊

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

為什麽要地址對齊

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

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

技術分享

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

技術分享

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

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

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

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

技術分享
 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類型,位段定義將變成一個普通的結構體,對齊原則也就遵從結構體的對齊原則。

#include <stdio.h>
#include <stdlib.h>
#include <stddef.h>
/************************************************************************/
/* 這裏約定:占用即表示本身大小及其後的空余空間
/************************************************************************/
struct MyStruct1 // 起始地址能被8整除
{
char a; // 8
double b; // 8
float c; // 4
int d; // 4
} m1; // 24

struct MyStruct2 // 起始地址能被8整除
{
int a; // 4
float b; // 4
char c; // 8 // 後面 double的起始地址要能被8 整除,所以c補齊8個字節
double d; // 8
} m2; // 24

struct MyStruct3 // 起始地址能被8整除
{
short a; // 2
char b; // 6 // 同理,後面的元素的起始地址要能被 8 整除,所以b只要占用6
double c; // 8
int d; // 8 // 需要在其後填充一些字節,以保證在分配數組之後,每個數組元素要滿足起始地址約束
} m3; // 24

struct MyStruct4
{
char a; // 2 // 能被4整除的地址 +2之後能被2整除,所以a只要補1個字節
short b; // 2
int c; // 4
} m4; // 8

struct MyStruct5 // 起始地址能被8整除
{
double a; // 8
float b; // 4
int c; // 4
short d; // 2
char e; // 6 因為後面緊緊挨著的MyStruct5 變量(在分配數組的時候)起始地址也要能被8整除,所以這個結構體總的大小必須是8的整數倍
} m5; // 24

struct MyStruct6 // 除4對齊
{
short a; // 2
char b; // 2
long c; // 4
short d; // 4 // 保證數組後面的元素也符合規則 (結構體首地址可以除4)
} m6; // 12

struct MyStruct7 // 4 對齊
{
int a; // 4
char b; // 2
short c; // 2
char d[6]; // 8
} m7; // 16

int main(){
printf("m1 size : %d\n",sizeof m1);
printf("m2 size : %d\n",sizeof m2);
printf("m3 size : %d\n",sizeof m3);
printf("m4 size : %d\n",sizeof m4);
printf("m5 size : %d\n",sizeof m5);
printf("m6 size : %d\n",sizeof m6);
printf("m7 size : %d\n",sizeof m7);

// offsetof 函數用來計算成員離結構體首地址偏移的字節數
printf("MyStruct1 b offset : %d\n",offsetof(struct MyStruct1,b)); // b偏移8個字節,所以成員a占用8個字節
printf("MyStruct2 d offset : %d\n",offsetof(struct MyStruct2,d)); // d偏移了16個字節
printf("MyStruct3 c offset : %d\n",offsetof(struct MyStruct3,c)); // 偏移8
printf("MyStruct4 b offset : %d\n",offsetof(struct MyStruct4,b)); // 偏移2
printf("MyStruct5 e offset : %d\n",offsetof(struct MyStruct5,e)); // 偏移16
printf("MyStruct6 c offset : %d\n",offsetof(struct MyStruct6,c)); // 偏移4
printf("MyStruct7 c offset : %d\n",offsetof(struct MyStruct7,c)); // 偏移
system("pause");
return 0;
}

測試代碼

技術分享 技術分享
 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 }

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