從硬件到語言,詳解C++的內存對齊


很多寫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的除外),很可能產生額外的開銷等。ARMv8中的Cortex-A系列是手機上常見的CPU家族,他們就可以正常處理未對齊內存訪問,但是一般會有額外的開銷。
我們也可以寫一個簡單的程序測試一下自己的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:

圖1
復制代碼
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等等,在此就不細述了。
最后你覺得我們的文章對你有幫助,歡迎關注我,可以私信我:久伴,領取學習資料,在評論下方可以關注我的學習群,你可以隨時在上面向我們提問,把你在學習C++過程中所遇到的問題發給我們。我們每天都會按時回復大家的每一個問題,希望久伴可以伴隨你從入門到專家。


注意!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系我们删除。



 
粤ICP备14056181号  © 2014-2021 ITdaan.com