內存對齊問題


基本數據類型的對齊問題:

變量在內存中的存放位置一般要求自然對齊。所謂自然對齊,就是基本數據類型的變量不能簡單地存儲在內存中任意的位置,而是其起始地址必須滿足可以被它們的大小整除。例如,32位平台下,int和指針類型變量的地址應該可以被4整除,short類型變量的地址應該可以被2整除,char和bool由於占用1個字節,因此相當於沒有對齊要求。

 

復合數據類型的對齊問題:

復合數據類型中,其中的成員必須滿足自然對齊規則,其中對齊時以相對地址為基。同時考慮到了可能使用該類型數組,因此編譯器會在最后一個變量末尾插入一定的字節(具體插入多少字節以復合類型中包含的最大基本類型為准)以保證即使使用該類型數組,也盡可能不影響訪問效率(一般數組中的元素都是連續存放的,如果復合類型所占空間滿足其包含的最大基本類型的倍數,那么即使使用復合類型數組,也可以較高的效率訪問數組中的下一個復合類型對象)。如下為一個示例程序:

 1 struct A
 2 {
 3         bool a;
 4         int b;
 5         bool c;
 6         double d;
 7         bool e;
 8 
 9 };
10 
11 int main()
12 {
13         struct A sa;
14         cout << sizeof(sa) << endl;
15         cout << "&sa.a = " << &sa.a << endl;
16         cout << "&sa.b = " << &sa.b << endl;
17         cout << "&sa.c = " << &sa.c << endl;
18         cout << "&sa.d = " << &sa.d << endl;
19         cout << "&sa.e = " << &sa.e << endl;
20 
21         return 0;
22 }
23 
24 // 32
25 // &sa = 0x22fee0
26 // &sa.a = 0x22fee0
27 // &sa.b = 0x22fee4
28 // &sa.c = 0x22fee8
29 // &sa.d = 0x22fef0
30 // &sa.e = 0x22fef8

分析:

變量sa的內存布局如下所示:

 

首先a的地址和結構體變量sa的地址是相同的。

a占用1個字節,地址為0x22fee0;

b占用4個字節,但是為了滿足自然對齊原則,其相對地址必須為4的倍數,因此最小的可使用地址為0x22fee4;

c占用1個字節,任何地址都滿足自然對齊原則,因此緊挨着b存放;

d占用8個字節,其起始地址必須滿足8的倍數,因此最小可用地址為0x22fef0;

e占用1個字節,但是為何其后又填充了7個多余字節呢?這是編譯器為了滿足struct A類型的數組對齊而設定的。試想一下,如果不填充這多余的7個字節,那么如果定義了struct A類型的數組,那么數組中的下一個元素的存放地址必然為ox22fef1,那么就不是自然對齊的,勢必會影響該數組的訪問效率。因此,一個復合類型的對象的存放地址必然為其中包含的最大類型所占字節的整數倍(假設其中包含的最大類型占用x個字節,那么該符合類型必須滿足x字節對齊)

 

復合類型對象在內存中創建后,每個成員的地址為相對地址(相對於該復合類型的起始地址),取決於相對該對象起始地址的偏移字節數。可以使用offsetof宏來查看不同成員相對於某個復合類型起始地址的偏移字節數:

 1 cout << "offsetof(A, a) = " << offsetof(A, a) << endl;
 2 cout << "offsetof(A, b) = " << offsetof(A, b) << endl;
 3 cout << "offsetof(A, c) = " << offsetof(A, c) << endl;
 4 cout << "offsetof(A, d) = " << offsetof(A, d) << endl;
 5 cout << "offsetof(A, e) = " << offsetof(A, e) << endl;
 6 
 7 // offsetof(A, a) = 0
 8 // offsetof(A, b) = 4
 9 // offsetof(A, c) = 8
10 // offsetof(A, d) = 16
11 // offsetof(A, e) = 24

 

用戶也可以為這個對象類型指定成員對其方式,可以使用#pragma編譯指令實現。實際應用中,最好顯式為每個復合數據類型指定對齊方式(但是要注意指定對齊方式后,雖然在某種程度上可以改善程序效率,但是不可移植),如下為示例代碼:

從上述程序可以看出,gcc編譯器默認是8字節對齊的。

 

由於在存儲變量時存在自然對齊和復合類型對齊這種約束,因此我們在設計一個復合類型時,盡量將占用空間比較大的成員安排到前面,這樣即使需要對齊,也是以小類型成員為主,有利於節約存儲空間,如:

 1 #pragma pack(push, 8)
 2 struct A
 3 {
 4         double d;
 5         int b;
 6         bool a;
 7         bool c;
 8         bool e;
 9 };
10 #pragma pack(pop)
11 // 這樣安排數據成員后,結構體所占空間變為16個字節,整整減少了一半。

 

綜上所述,可以看出影響對象實際大小和訪問效率的因素包括:數據成員類型、聲明順序、對齊方式同時不同的對齊方式還會影響接口之間的語義一致性和二進制兼容性,比如一個應用程序包含若干個模塊,而不同模塊使用的對齊方式不同,如果此時模塊之間共享的復合數據類型也沒有顯式指定對齊方式,那么程序出錯的概率就大大增加了,因為此時不同模塊對於同一個共享數據的解釋方式不一樣了。

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM