|
什么是內(nèi)存對齊
考慮下面的結(jié)構(gòu): struct foo { char c1; short s; char c2; int i; }; 假設這個結(jié)構(gòu)的成員在內(nèi)存中是緊湊排列的,假設c1的地址是0,那么s的地址就應該是1,c2的地址就是3,i的地址就是4。也就是 c1 00000000, s 00000001, c2 00000003, i 00000004。 可是,我們在Visual c/c++ 6中寫一個簡單的程序: struct foo a; printf("c1 %p, s %p, c2 %p, i %p\n", (unsigned int)(void*)&a.c1 - (unsigned int)(void*)&a, (unsigned int)(void*)&a.s - (unsigned int)(void*)&a, (unsigned int)(void*)&a.c2 - (unsigned int)(void*)&a, (unsigned int)(void*)&a.i - (unsigned int)(void*)&a); 運行,輸出: c1 00000000, s 00000002, c2 00000004, i 00000008。 為什么會這樣?這就是內(nèi)存對齊而導致的問題。 為什么會有內(nèi)存對齊 以下內(nèi)容節(jié)選自《Intel Architecture 32 Manual》。 字,雙字,和四字在自然邊界上不需要在內(nèi)存中對齊。(對字,雙字,和四字來說,自然邊界分別是偶數(shù)地址,可以被4整除的地址,和可以被8整除的地址。) 無論如何,為了提高程序的性能,數(shù)據(jù)結(jié)構(gòu)(尤其是棧)應該盡可能地在自然邊界上對齊。原因在于,為了訪問未對齊的內(nèi)存,處理器需要作兩次內(nèi)存訪問;然而,對齊的內(nèi)存訪問僅需要一次訪問。 一個字或雙字操作數(shù)跨越了4字節(jié)邊界,或者一個四字操作數(shù)跨越了8字節(jié)邊界,被認為是未對齊的,從而需要兩次總線周期來訪問內(nèi)存。一個字起始地址是奇數(shù)但卻沒有跨越字邊界被認為是對齊的,能夠在一個總線周期中被訪問。 某些操作雙四字的指令需要內(nèi)存操作數(shù)在自然邊界上對齊。如果操作數(shù)沒有對齊,這些指令將會產(chǎn)生一個通用保護異常(#GP)。雙四字的自然邊界是能夠被16整除的地址。其他的操作雙四字的指令允許未對齊的訪問(不會產(chǎn)生通用保護異常),然而,需要額外的內(nèi)存總線周期來訪問內(nèi)存中未對齊的數(shù)據(jù)。 編譯器對內(nèi)存對齊的處理 缺省情況下,c/c++編譯器默認將結(jié)構(gòu)、棧中的成員數(shù)據(jù)進行內(nèi)存對齊。因此,上面的程序輸出就變成了: c1 00000000, s 00000002, c2 00000004, i 00000008。 編譯器將未對齊的成員向后移,將每一個都成員對齊到自然邊界上,從而也導致了整個結(jié)構(gòu)的尺寸變大。盡管會犧牲一點空間(成員之間有空洞),但提高了性能。 也正是這個原因,我們不可以斷言sizeof(foo) == 8。在這個例子中,sizeof(foo) == 12。 如何避免內(nèi)存對齊的影響 那么,能不能既達到提高性能的目的,又能節(jié)約一點空間呢?有一點小技巧可以使用。比如我們可以將上面的結(jié)構(gòu)改成: struct bar { char c1; char c2; short s; int i; }; 這樣一來,每個成員都對齊在其自然邊界上,從而避免了編譯器自動對齊。在這個例子中,sizeof(bar) == 8。 這個技巧有一個重要的作用,尤其是這個結(jié)構(gòu)作為API的一部分提供給第三方開發(fā)使用的時候。第三方開發(fā)者可能將編譯器的默認對齊選項改變,從而造成這個結(jié)構(gòu)在你的發(fā)行的DLL中使用某種對齊方式,而在第三方開發(fā)者哪里卻使用另外一種對齊方式。這將會導致重大問題。 比如,foo結(jié)構(gòu),我們的DLL使用默認對齊選項,對齊為 c1 00000000, s 00000002, c2 00000004, i 00000008,同時sizeof(foo) == 12。 而第三方將對齊選項關閉,導致 c1 00000000, s 00000001, c2 00000003, i 00000004,同時sizeof(foo) == 8。 如何使用c/c++中的對齊選項 vc6中的編譯選項有 /Zp[1|2|4|8|16] ,/Zp1表示以1字節(jié)邊界對齊,相應的,/Zpn表示以n字節(jié)邊界對齊。n字節(jié)邊界對齊的意思是說,一個成員的地址必須安排在成員的尺寸的整數(shù)倍地址上或者是n的整數(shù)倍地址上,取它們中的最小值。也就是: min ( sizeof ( member ), n) 實際上,1字節(jié)邊界對齊也就表示了結(jié)構(gòu)成員之間沒有空洞。 /Zpn選項是應用于整個工程的,影響所有的參與編譯的結(jié)構(gòu)。 要使用這個選項,可以在vc6中打開工程屬性頁,c/c++頁,選擇Code Generation分類,在Struct member alignment可以選擇。 要專門針對某些結(jié)構(gòu)定義使用對齊選項,可以使用#pragma pack編譯指令。指令語法如下: #pragma pack( [ show ] | [ push | pop ] [, identifier ] , n ) 意義和/Zpn選項相同。比如: #pragma pack(1) struct foo_pack { char c1; short s; char c2; int i; }; #pragma pack() 棧內(nèi)存對齊 我們可以觀察到,在vc6中棧的對齊方式不受結(jié)構(gòu)成員對齊選項的影響。(本來就是兩碼事)。它總是保持對齊,而且對齊在4字節(jié)邊界上。 驗證代碼 #include <stdio.h> struct foo { char c1; short s; char c2; int i; }; struct bar { char c1; char c2; short s; int i; }; #pragma pack(1) struct foo_pack { char c1; short s; char c2; int i; }; #pragma pack() int main(int argc, char* argv[]) { char c1; short s; char c2; int i; struct foo a; struct bar b; struct foo_pack p; printf("stack c1 %p, s %p, c2 %p, i %p\n", (unsigned int)(void*)&c1 - (unsigned int)(void*)&i, (unsigned int)(void*)&s - (unsigned int)(void*)&i, (unsigned int)(void*)&c2 - (unsigned int)(void*)&i, (unsigned int)(void*)&i - (unsigned int)(void*)&i); printf("struct foo c1 %p, s %p, c2 %p, i %p\n", (unsigned int)(void*)&a.c1 - (unsigned int)(void*)&a, (unsigned int)(void*)&a.s - (unsigned int)(void*)&a, (unsigned int)(void*)&a.c2 - (unsigned int)(void*)&a, (unsigned int)(void*)&a.i - (unsigned int)(void*)&a); printf("struct bar c1 %p, c2 %p, s %p, i %p\n", (unsigned int)(void*)&b.c1 - (unsigned int)(void*)&b, (unsigned int)(void*)&b.c2 - (unsigned int)(void*)&b, (unsigned int)(void*)&b.s - (unsigned int)(void*)&b, (unsigned int)(void*)&b.i - (unsigned int)(void*)&b); printf("struct foo_pack c1 %p, s %p, c2 %p, i %p\n", (unsigned int)(void*)&p.c1 - (unsigned int)(void*)&p, (unsigned int)(void*)&p.s - (unsigned int)(void*)&p, (unsigned int)(void*)&p.c2 - (unsigned int)(void*)&p, (unsigned int)(void*)&p.i - (unsigned int)(void*)&p); printf("sizeof foo is %d\n", sizeof(foo)); printf("sizeof bar is %d\n", sizeof(bar)); printf("sizeof foo_pack is %d\n", sizeof(foo_pack)); return 0; } vc6中的編譯選項有 /Zp[1|2|4|8|16] ,/Zp1表示以1字節(jié)邊界對齊,相應的,/Zpn表示以n字節(jié)邊界對齊。
n字節(jié)邊界對齊的意思是說,一個成員的地址必須安排在成員的尺寸的整數(shù)倍地址上或者是n的整數(shù)倍地址上,取它們中的最小值。也就是:
min ( sizeof ( member ), n) 實際上,1字節(jié)邊界對齊也就表示了結(jié)構(gòu)成員之間沒有空洞。 /*1字節(jié)邊界對齊表示結(jié)構(gòu)成員之間沒有空隙,個個成員變量在內(nèi)存中是緊密排列的*/ /Zpn選項是應用于整個工程的,影響所有的參與編譯的結(jié)構(gòu)。 |
|
|
來自: thunder123 > 《C/C 》