C/C++記憶體對齊
what && why
當使用者自定義型別時(struct 或 class),編譯器會自動計算該型別佔用的位元組數。
C/C++ 為什麼要記憶體對齊?我道行太淺,摘抄了網上的一個解釋。
為了方便從記憶體中讀取資料。假設沒有記憶體對齊,在記憶體中儲存一個 int 變數 x(佔 4 位元組),放在了地址 2-5 上。現在要讀取 x 到暫存器中,CPU 知道讀 int 一次應該讀 4 位元組,但是不會直接讀地址 2-5(為什麼不會?我也不知道啊!但是 CPU 有直接讀 2-5 地址的功能,但它沒有用起來),一次讀出來,而是先讀 0-3,再讀 4-7,丟掉多餘的位元組。可以看到對齊後少讀了一次記憶體,效能肯定得到提升了(我們知道 C/C++ 是追求極致效能的)。
舉例
#include <iostream>
using namespace std;
// #pragma pack (1)
struct Test
{
int i1;
char c;
int i2;
double d;
};
int main(int argc, char* argv[])
{
cout << sizeof(Test) << endl; // 24
return 0;
}
如果沒有記憶體對齊,Test 型別的大小應該是 4+1+4+8 = 17
位元組,經過對齊後變成了 24 位元組。
第 5 行註釋就是設定記憶體對齊基數,取值一般是 1, 2, 4, 8,若該值為 1 則表示不對齊(不信就去掉註釋再執行一次,輸出肯定是 17)。
記憶體對齊原則
- 整體對齊基數 n:假設預設或透過
#pragma pack ()
設定的對齊基數是 i(現在機器一般都是 8,舊一些的應該是 4),struct 中“最大”成員所佔用的位元組數 j,則n = min(i, j)
,也就是說這個 struct 型別最終的大小必須是 n 的倍數。 - 成員對齊基數 k:它的計算方式是
k = min(sizeof(memberType), n)
,它要求每個成員的 offset 必須是 k 的倍數,第一個成員的 offset 為 0。比如一個 short 成員的k = min(sizeof(short), n)
可以看出,當 i = 1
時就是不對齊;當 i >= j
時,i 不起作用。
操練一下
假設 n = 8
先進行成員對齊:
#include <iostream>
using namespace std;
struct Test
{
int i1; // offset為0, 佔用第0-3位元組
char c; // 1 < 8, offset是1的倍數, 因此offset為4, 佔用第4位元組
int i2; // 4 < 8, offset是4的倍數, 因此offset為8, 佔用第8-11位元組
double d; // 8 == 8, offset是8的倍數, 因此offset為16, 佔用第16-23位元組
// 建構函式
Test(int ii1, char cc, int ii2, double dd):
i1(ii1), c(cc), i2(ii2), d(dd) {}
};
// 來驗證一下
int main(int argc, char* argv[])
{
cout << sizeof(Test) << endl;
Test *pt = new Test(1, 'a', 2, 1.25); // 基地址
unsigned char* ppt = (unsigned char*)pt; // 強制型別轉換, 按位元組讀
for (int i = 0; i < sizeof(Test); ++i) {
printf("%x ", *(ppt + i));
}
cout << endl;
// 1 0 0 0 61 f0 ad ba 2 0 0 0 d f0 ad ba 0 0 0 0 0 0 f4 3f
return 0;
}
再進行整體對齊:這個 struct 型別所需位元組為 24 位元組,恰好是 n 的倍數,無須在尾部額外填充。
記憶體排列如下圖所示:
其中白色格子代表填充,其內容是不確定的。
按十六進位制輸出:1 0 0 0 61 f0 ad ba 2 0 0 0 d f0 ad ba 0 0 0 0 0 0 f4 3f
-
可以看到前面 4 位元組是 1 0 0 0,是
i1 = 1
; -
第 5 位元組是 61,是
'a'
的十六進位制 ASCII 碼; -
然後 6-7 位元組是填充的內容,不確定的;
-
第 8-11 位元組是 2 0 0 0,是
i2 = 2
; -
第 12 - 15 位元組是填充的內容,不確定的;
-
第 16-23 位元組是
d = 1.25
的底層二進位制表示(怎麼算的我也忘了好久了,參考神書《CSAPP:深入理解計算機系統》即可找回記憶)。
留下疑問
問:在自定義型別巢狀時,比如 Test1 巢狀正在 Test2 中,此時應該怎麼進行記憶體對齊呢?
struct Test1
{
int i1;
char c;
int i2;
double d;
// 建構函式
Test1(int ii1, char cc, int ii2, double dd):
i1(ii1), c(cc), i2(ii2), d(dd) {}
};
struct Test2
{
Test1 t1;
int x;
};
答:先計算 Test1 所佔位元組大小 sizeof(Test1)
,然後繼續按照上述基本原則計算 Test2 即可。如果是多重巢狀,那就遞迴找到那個成員全都是基本型別的 struct 開始計算,然後回溯。
問:繼承體系中如何進行記憶體對齊?
struct A
{
int i;
char c1;
};
struct B: public A
{
char c2;
};
struct C: public B
{
char c3;
};
答:我也不會!我鬱悶了,在我 64 位 Windows 作業系統 + gcc8.1.0 和 ubuntu18.04 + gcc7.5.0 上的執行結果都是 12!
但是我參考的一篇部落格說,他的結果是 8 或 16!C++ 記憶體對齊 - tenos - 部落格園 (cnblogs.com)
部落格裡說根據編譯器型別擁有兩種方式:先繼承後對齊和先對齊後繼承。
但是我無論按哪種方式,#pragma pack ()
取 4 或 8,排列組合 2*2=4 種可能,我都算不出來 12!但是我能算出 8 和 16!
希望有朋友可以解答我的疑惑,萬分感謝。
最後
如果本文對你有幫助,請點個贊吧。
有任何疑問,歡迎評論和我一起討論。