對內存對齊的深一步理解

  接觸內存對齊這個概念,也有三四年了。不過由於我工作後一直做遊戲服務器,都是在x86架構的機子上寫代碼,也沒怎麼注意內存對齊。使用最多的估計也就是面試時經常問結構體大小。最近在寫自己服務器框架的二進流讀寫模塊時,整理了下這方面的內容。本方不會涉及基本概念。

  內存對齊只是指數據存儲在內存時的起始地址是否是某個值的整數倍。如果只是放在內存中,是否對齊本身並沒有什麼問題。問題是讀取、寫入的時候。訪問一個不對齊的數據(unaligned memory access)可能會導致程序運行效率慢,結果出錯,甚至是程序當掉。那這些情況是怎麼出現的呢?

  我們都知道,程序最終都是以CPU指令來運行的。參考:http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.faqs/ka15414.html,我們知道ARM CPU有下面幾條指令:

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)

這是強制把這個結構體按1byte對齊,當有網絡數據過來,直接memcpy整個結構體就可以。有趣的時,我在內核文檔裏發現這麼一段話:https://www.kernel.org/doc/Documentation/unaligned-memory-access.txt

複製代碼

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對齊時,編譯器不會在結構體中加入任何內容來使得這個結構體符合內存對齊,而是產生一些額外的指令來讓他滿足當前平臺的內存對齊,當然,效率還是受影響的。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章