摘要訊息 : 對位的記憶體能夠極大地提升程式效能.
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. 如若閣下需要轉發, 在已經授權的情況下請註明本文出處 :