很多寫C/C++的人都知道“記憶體對齊”的概念以及規則,但不一定對他有很深入的瞭解。這篇文章試著從硬體到C++語言、更徹底地講一下C++的記憶體對齊。 ...
轉載請保留以下聲明
作者:趙宗晟
出處:https://www.cnblogs.com/zhao-zongsheng/p/9099603.html
很多寫C/C++的人都知道“記憶體對齊”的概念以及規則,但不一定對他有很深入的瞭解。這篇文章試著從硬體到C++語言、更徹底地講一下C++的記憶體對齊。
什麼是記憶體對齊(memory alignment)
首先,什麼是記憶體對齊(memory alignment)?這個是從硬體層面出現的概念。大家都知道,可執行程式是由一系列CPU指令構成的。CPU指令中有一些指令是需要訪問記憶體的。最常見的就是“從記憶體讀到寄存器”,以及“從寄存器寫到記憶體”。在老的架構中(包括x86),也有一些運算的指令是可以直接以記憶體為操作數,那麼這些指令也隱含了記憶體的讀取。在很多CPU架構下,這些指令都要求操作的記憶體地址(更準確的說,操作記憶體的起始地址)能夠被操作的記憶體大小整除,滿足這個要求的記憶體訪問叫做訪問對齊的記憶體(aligned memory access),否則就是訪問未對齊的記憶體(unaligned memory access)。舉例來說,ARM的LDRH指令從記憶體中讀取2個byte到寄存器中。如果指定的記憶體的地址是0x2587c20,因為0x2587c20這個數能夠被2整除,所以這2個byte是對齊的。而如果指定的記憶體的地址是0x2587c33,因為不能被2整除,所以是未對齊的。
那如果訪問未對齊的記憶體會出現什麼結果呢?這個要看CPU。
- 有些CPU架構可以訪問未對齊的記憶體,但是會有性能上的影響。典型的就是x86架構CPU
- 有些CPU會拋出異常
- 還有些CPU不會拋出任何異常,會靜默地訪問錯誤的地址
- 近幾年也有些CPU的一部分指令可以正常訪問未對齊的記憶體,同時不會有性能影響
因為每個CPU對未對齊記憶體的訪問的處理方式都不一樣,所以訪問未對齊的記憶體是要儘量避免的。所以就出現了C/C++的記憶體對齊機制。
C++的記憶體對齊機制
在C++中每個類型都有兩個屬性,一個是大小(size),還有一個就是對齊要求(alignment requirement),或稱之為對齊量(alignment)。C++標準並沒有規定每個類型的對齊量,但是一般都會有這樣的規律。
- 所有基礎類型的對齊量等於這個類型的大小。
- struct, class, union類型的對齊量等於他的非靜態成員變數中最大的對齊量。
另外,標準規定所有的對齊量必須是2的冪。
編譯器在給一個變數分配記憶體時,都要算出並滿足這個類型的對齊要求。struct和class類型的非靜態成員變數的位元組數偏移(offset)也要滿足各自類型的對齊要求。
舉例來說,
class MyObject { char c; int i; short s; };
c是char類型,對齊要求是1,i是int類型,對齊要求是4,s是short類型,對齊要求是2。那麼MyObject取最大的,也就是4作為他的對齊要求。如果在某個函數中聲明瞭MyObject類型的變數,那麼分配給這個變數的記憶體的起始地址是能夠被4整除的。
我們再看MyObject的成員變數。c是MyObject的第一個成員變數,所以他的位元組數偏移是0,也就是說變數c占據MyObject的第一個byte。i的對齊要求是4,所以位元組數偏移必須是4的倍數,又因為變數i必須在變數c的後面,於是i的位元組數偏移就是4,也就是說變數i占據MyObject的第5到第8個byte,而第2到第4個byte則是空白填充(padding)。s的對齊要求是2,又因為s必須在i的後面,所以s的位元組數偏移是8,也就是說,變數s占據MyObject的第9個和第10個byte。另外,因為struct、class、union類型的數組的每個元素都要記憶體對齊,所以一般來說struct、class、union的大小都是這個類型的對齊量的整數倍,所以MyObject的大小是12,也就是說,變數s後面會有2個byte的空白填充。
因為C++中所有記憶體訪問都是通過變數的讀寫來訪問的,這個機制確保了所有變數都滿足了記憶體對齊,也就確保了程式中所有記憶體訪問都是對齊的。
當然,C++不會阻止我們去訪問未對齊的記憶體。例如,以下的代碼就很可能會訪問未對齊的記憶體:
char buf[10]; int* ptr = (int*)(buf + 1); ++*ptr;
這類代碼是我們在實際工作中也是能遇到的。事實上這種寫法是比較危險的,因為他很可能會去訪問未對齊的記憶體。這也是為什麼寫c++大家都不推薦用c風格的類型轉換寫法,而是要用static_cast, dynamic_cast, const_cast與reinterpret_cast。這樣的話,上面的代碼就必須要使用reinterpret_cast,大家都知道reinterpret_cast是很危險的,也許就會想辦法避免這樣的邏輯。
常見CPU的未對齊記憶體訪問
根據Intel最新的Intel 64及IA-32架構說明書,Intel 64及IA-32架構都支持未對齊記憶體的訪問,但是會有性能上的額外開銷(詳見http://www.intel.com/products/processor/manuals)。但是實際上最近的Core系列CPU已經可以無額外開銷訪問未對齊的記憶體。
而手機上最常見的ARMv8架構,如果是普通的、不做多核同步的未對齊的記憶體訪問,那麼CPU可能會產生對齊錯誤(alignment fault)或者執行未對齊記憶體操作。換句話說,到底會報錯還是正常執行,是要看具體CPU的實現的。即使是執行正常操作,也會有一些限制。例如,不能保證讀寫的原子性(操作一個byte的除外),很可能產生額外的開銷等(詳見https://developer.arm.com/docs/ddi0487/latest/arm-architecture-reference-manual-armv8-for-armv8-a-architecture-profile)。ARMv8中的Cortex-A系列是手機上常見的CPU家族,他們就可以正常處理未對齊記憶體訪問,但是一般會有額外的開銷(詳見http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.faqs/ka15414.html)。
我們也可以寫一個簡單的程式測試一下自己的CPU對未對齊記憶體訪問的支持,以下是代碼:
#include <iostream> #include <chrono> using namespace std; using namespace std::chrono; milliseconds test_duration(volatile int * ptr) // 使用volatile指針防止編譯器的優化 { auto start = steady_clock::now(); for (unsigned i = 0; i < 100'000'000; ++i) { ++(*ptr); } auto end = steady_clock::now(); return duration_cast<milliseconds>(end - start); } int main() { int raw[2] = {0, 0}; { int* ptr = raw; cout << "address of aligned pointer: " << (void*)ptr << endl; cout << "aligned access: " << test_duration(ptr).count() << "ms" << endl; *ptr = 0; } { int* ptr = (int*)(((char*)raw) + 1); cout << "address of unaligned pointer: " << (void*)ptr << endl; cout << "unaligned access: " << test_duration(ptr).count() << "ms" << endl; *ptr = 0; } cin.get(); return 0; }
我測試使用的電腦的CPU是Intel Core i7 2630QM,是intel 2代酷睿CPU,測試結果為:
address of aligned pointer: 000000668DEFFA78
aligned access: 282ms
address of unaligned pointer: 000000668DEFFA79
unaligned access: 285ms
可以看出對齊與未對齊的記憶體訪問沒有性能上的差別。
在C++中修改對齊要求
一般情況下,我們不需要自定義對齊要求,但也會有很特殊的情況下需要做調整。C++中,我們可以使用alignas關鍵字修改一個類型、或者一個變數的對齊要求。例如:
class MyObject { char c; alignas(8) int i; short s; };
這樣的話,變數i的對齊要求由原本的4變成了8,結果就是,i的位元組數偏移由4變成了8,s的位元組數偏移由8變成了12,MyObject的對齊要求也變成了8,大小變成了16。
我們也可以對MyObject的定義使用alignas:
class alignas(16) MyObject { char c; int i; short s; };
還可以在alignas裡面寫某個類型。也可以使用多個alignas,結果就是使用最大的對齊要求。例如以下MyObject的對齊要求就是16:
class alignas(int) alignas(16) MyObject { char c; int i; short s; };
alignas有一個限制,那就是不能用alignas改小對齊要求。例如以下的代碼會報錯:
alignas(1) int i;
另外,C++中,有一個特殊的類型:max_align_t,所有不大於他的對齊量叫做基礎對齊量(fundamental alignment),比這個對齊量大的叫做擴展對齊量(extended alignment )。C++標準規定,所有平臺必須要支持基礎對齊量,而對於擴展對齊量的支持要看各個平臺。一般來說max_align_t的對齊量等於long double的對齊量。
C++關於記憶體對齊的支持還有很多功能,例如查詢對齊量的alignof關鍵字,可以創建任意大小任意對齊要求的類型的aligned_storage模板,還有方便模板編程的alignment_of等等,在此就不細述了。