C/C++記憶體對齊原則

2023-02-05 15:00:34

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)。

記憶體對齊原則

  1. 整體對齊基數 n:假設預設或通過#pragma pack ()設定的對齊基數是 i(現在機器一般都是 8,舊一些的應該是 4),struct 中「最大」成員所佔用的位元組數 j,則 n = min(i, j),也就是說這個 struct 型別最終的大小必須是 n 的倍數
  2. 成員對齊基數 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 的倍數,無須在尾部額外填充。

記憶體排列如下圖所示:

image-20230205113740178

其中白色格子代表填充,其內容是不確定的。

按十六進位制輸出: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!

希望有朋友可以解答我的疑惑,萬分感謝。

最後

如果本文對你有幫助,請點個贊吧。

有任何疑問,歡迎評論和我一起討論。