摘要訊息 : 對位的記憶體能夠極大地提升程式效能.
0. 前言
C 和 C++ 都有記憶體對位的機制, 很多程式設計語言也都繼承了這個機制. 記憶體是否對位對於程式效能的影響很大. 在 C++ 11 還引入了標識符 alignas 與運算子 alignof, 這兩個是專門處理記憶體對位的關鍵字.
對於結構體
struct Foo {
char c[3];
int i;
};
來說, sizeof(Foo) 的值是多少呢? 在 64 位元的作業系統下, char 型別的大小為 1, int 型別的大小為 4, 那麼 sizeof(Foo) 的值本應該是 7. 然而預設情況下, sizeof(Foo) 的值是 8. 接下來我們就來分析 C++ 中的記憶體對位.
更新紀錄 :
- 2022 年 4 月 26 日進行第一次更新和修正.
1. 記憶體對位
對於某一個固定的型別, 硬體是按照一定的倍數對記憶體進行存取的. 例如對於 64 位元的作業系統來說, 一個 int 型別的大小為 4. 因此, 硬體是按照 4 的倍數進行存取的. 若從記憶體位址 0x000000000010 (簡寫為 0x10, 這是一個十六進制數) 開始, 存取一個 int 型別的變數, 那麼這個 int 型別的變數在記憶體中儲存的位址是 0x10 - 0x13. 如果 0x14 上也儲存著一個 int 型別的變數, 那麼其儲存的記憶體位址是 0x14 - 0x17.
給定一個記憶體位址和型別, 要測試是否對位, 我們可以使用 \displaystyle {\text {記憶體位址 } \mod \text { 型別大小}}. 當結果為 0, 則說明記憶體對位; 否則, 就說明記憶體不對位.
那麼記憶體對位為什麼那麼重要呢? 假設現在一個 int 變數儲存在 0x6 - 0x9 上, 要存取這個變數, 硬體首先會存取 0x4, 向後存取四個位元得到 0x6 和 0x7 中的資料; 然後存取 0x8, 向後存取四個位元得到 0x8 和 0x9 中的資料, 最後將 0x6, 0x7 和 0x8, 0x9 這兩段資料拼接起來的到一個 int 的值. 如果這個 int 變數儲存在 0x4 上, 那麼只需要存取 0x4 開始的四個位元中的資料即可, 不需要拼接.
記憶體對位與不對位相比較, 首先是效率問題. 不對位的記憶體要存取的次數比對位的記憶體要存取的次數多, 另外不對位的記憶體資料需要進行拼接. 然後是出錯的問題, 對於現代的民用電腦來說, 很少會存在這種問題, 但是對於有些 CPU 架構, 若存取的記憶體並不是對位的, 那麼硬體可能會擲出例外情況甚至拒絕存取的請求.
C/C++ 中的記憶體對位是以 2^{k} 為基礎, 並且以結構體或者類別中 sizeof 值最大的那個型別為基底. 例如第 0 節中的結構體 Foo 以 int 作為基底, 對位的大小為 4 = 2^{2}. 結構體
struct s1 {
short a;
struct {
char a[16];
int b;
} b;
};
是以 int 對位的, 所以 sizeof(s1) 的值為 24. 那麼成員變數 a 之後就會有 2 位元組的資料一直是空的, 如果不想浪費的話我們可以改為
struct s2 {
short a;
short b;
struct {
char a[16];
int b;
} c;
};
sizeof(s2) 的值仍然為 24. 將巢狀類別中的 b 的型別從 int 修改為 long, 那麼 sizeof(s2) 的值會變成 32. 此時, s2 的成員變數 b 之後會有 4 位元組的資料一直是空的.
2. #pragma pack
有些時候, 我們可能需要儘量節省記憶體, 寧可用拼接資料的時間來換取記憶體. 這個時候, 我們需要借助前處理指令 #pragma pack(n). 其中, n 小於 0 的話, 編碼器會擲出編碼警告並且忽略這條前處理指令. 另外, 如果宣告 #pragma pack(0), 那麼編碼器就會按照預設的對位方案進行對位.
對於第 1 節中的結構體 s1, 我們說過, 成員變數 b 後面有兩個位元組是空著的, 為了節省這四個位元組的記憶體, 我們可以宣告 #pragma pack(1). 這樣, sizeof(s1) 的值為 22.
3. alignas 和 alignof
有時候, 我們想要將記憶體的對位擴大, 例如第 1 節中的結構體 s2, 它是以 int 型別的大小作為對位基底的, 如果我們想要在不改變 s2 的情況下, 把記憶體對位擴大到十六位元組, 那麼可以使用 alignas 標識符. alignas 標識符接受一個參數, 這個參數可以是型別, 表達式和整型常數表達式. 對於 alignas(T), 就是讓類別的對位和型別 T 相同; 對於 alignas(expression), 就是讓類別的對位和表達式 expression 對應的型別 decltype(expression) 相同; 對於 alignas(n), 就是設定類別的記憶體對位大小為 n. 那麼 s2 可以寫成
struct alignas(long) s2 {
short a;
short b;
struct {
char a[16];
int b;
} c;
};
此時, sizeof(s2) 的值為 32, 不再是 24, 因為 24 不是 16 的倍數.
如果要查詢一個類別的記憶體對位情況, 可以使用 alignof 運算子, 它的用法和 sizeof 類似. 唯一的不同是, C++ 標準暫時還不允許計算一個表達式的記憶體對位情況, 即 alignof(expression) 或者 alignof expression 是暫時不被允許的. 但是部分編碼器 (例如 Apple Clang 和 GCC) 提供了擴展, 讓 alignof 也可以支援表達式的記憶體對位計算.
在對類別使用 alignas 標識符的時候, 需要注意, 如果 alignas 中的值小於類別中任意成員的 alignof 值, 那麼就會產生編碼錯誤. 所以說, alignas 只能擴大記憶體對位, 不能像 #pragma pack 那樣縮小記憶體對位.
alignas 允許標識在結構體, 類別, 等位和列舉中, 也可以標識在一個普通變數上, 例如 alignas(8) int a;. 這樣, sizeof a 的值是 4, alignof a (若允許計算) 的值為 8.
自創文章, 原著 : Jonny. 如若閣下需要轉發, 在已經授權的情況下請註明本文出處 :