對記憶體對齊的深一步理解
接觸記憶體對齊這個概念,也有三四年了。不過由於我工作後一直做遊戲伺服器,都是在x86架構的機子上寫程式碼,也沒怎麼注意記憶體對齊。使用最多的估計也就是面試時經常問結構體大小。最近在寫自己伺服器框架的二進流讀寫模組時,整理了下這方面的內容。本方不會涉及基本概念。
記憶體對齊只是指資料儲存在記憶體時的起始地址是否是某個值的整數倍。如果只是放在記憶體中,是否對齊本身並沒有什麼問題。問題是讀取、寫入的時候。訪問一個不對齊的資料(unaligned memory access)可能會導致程式執行效率慢,結果出錯,甚至是程式當掉。那這些情況是怎麼出現的呢?
1 2 3 |
LDRB /STRB -
address must be byte aligned
LDRH /STRH -
address must be 2-byte aligned
LDR /STR -
address must be 4-byte aligned
|
LDRB/STRB位元組載入、儲存指令
LDRH/STRH半字(即2byte,不是半位元組)載入、儲存指令
LDR/STR 字載入、儲存指令
也就是說,當我們從記憶體中存取資料時,要呼叫上面的指令。而這些指令在設計時,較老的CPU由於考慮了硬體、效率等等問題,要求訪問的記憶體必須是對齊的。現在假如我聲明瞭一個記憶體緩衝區char *buffer[1024],系統給它分配的地址是0x00001000,可以看到,這個地址都是符合1、2、4位元組對齊的。接著我從網路接收了一段資料,放到這個緩衝區裡。現在要從緩衝區裡依次取出char、int兩個型別的資料:
1 2 |
char ch
= *buffer;
int i
= * reinterpret_cast < int *>(buffer+1);
|
執行ch = *buffer時,由於char型別的大小是1位元組,CPU將呼叫LDRB指令,這時將檢測buffer是否按1byte對齊。這裡當然是對齊的,所以指令執行正常。
執行i = *reinterpret_cast<int *>(buffer+1)時,由於int型別大小是4位元組,CPU將呼叫LDR指令,這時檢測buffer+1(0x00001001)是否按4byte對齊,結果發現不對齊,CPU將報錯,程式中止。
而安全的做法是這樣的:
1 2 |
memcpy (
&ch,buffer,1 );
memcpy (
&i,buffer+1,4 );
|
你可能會問,使用memcpy,buffer+1的地址也是不對齊的,為什麼就安全了呢?就像我上面所說的,資料在記憶體中存放時,是否對齊並不重要,重要的是你怎樣去訪問它。memcpy的實現本身並不簡單(你在原始碼裡看到的通過while每次拷貝一個char的只是一個例子,並不是真實的memcpy),它考慮了是否對齊。當檢測到記憶體是對齊時,memcpy呼叫合適的指令(比較這裡拷貝一個int,就呼叫LDR),一次拷貝多個位元組,以提高效率。當檢測到不對齊時,先呼叫LDRB遂個位元組拷貝,直到對齊部分後再呼叫合適的指令拷貝。因此,在上面的例子中,它是先呼叫LDRB的,因為LDRB是按1byte對齊(所有的記憶體都按這個對齊),所以不會觸發報錯。但效率就要慢一點了,畢竟要拷貝幾次。
記憶體對齊本身對程式設計師來說是透明的,即程式設計師該取變數就取變數,該存就存,編譯程式時編譯器會把變數按本身的平臺進行對齊。況且現在的CPU都很高階,別說伺服器,桌上型電腦的CPU,ARM 7以上應該也支援記憶體不對齊訪問了。但如果你要寫一個記憶體池(boost的ordered_pool有對齊的例子),或者使用了reinterpret_cast這種對記憶體直接進行操作的函式,這方面還是要注意一下,即使CPU支援,效率也會受到影響。
我在很多專案中,發現這樣的寫法:
1 2 3 4 5 6 |
#pragma
pack(push,1)
struct NetPack
{
//...
};
#pragma
pack(pop)
|
Another point worth mentioning is the use of __attribute__((packed)) on a structure type. This GCC-specific attribute tells the compiler never to insert any padding within structures, useful when you want to use a C struct to represent some data that comes in a fixed arrangement 'off the wire'. You might be inclined to believe that usage of this attribute can easily lead to unaligned accesses when accessing fields that do not satisfy architectural alignment requirements. However, again, the compiler is aware of the alignment constraints and will generate extra instructions to perform the memory access in a way that does not cause unaligned access. Of course, the extra instructions obviously cause a loss in performance compared to the non-packed case, so the packed attribute should only be used when avoiding structure padding is of importance.
當我們把變數強制按1byte對齊時,編譯器不會在結構體中加入任何內容來使得這個結構體符合記憶體對齊,而是產生一些額外的指令來讓他滿足當前平臺的記憶體對齊,當然,效率還是受影響的