"資料的的對齊"
以下内容節選自《Intel Architecture 32 Manual》。
字,雙字,和四字在自然邊界上不需要在記憶體中對齊。(對字,雙字,和四字來說,自然邊界分别是偶數位址,可以被4整除的位址,和可以被8整除的位址。)
無論如何,為了提高程式的性能,資料結構(尤其是棧)應該盡可能地在自然邊界上對齊。原因在于,為了通路未對齊的記憶體,處理器需要作兩次記憶體通路;然而,對齊的記憶體通路僅需要一次通路。
一個字或雙字操作數跨越了4位元組邊界,或者一個四字操作數跨越了8位元組邊界,被認為是未對齊的,進而需要兩次總線周期來通路記憶體。一個字起始位址是奇
數但卻沒有跨越字邊界被認為是對齊的,能夠在一個總線周期中被通路。某些操作雙四字的指令需要記憶體操作數在自然邊界上對齊。如果操作數沒有對齊,
這些指令将會産生一個通用保護異常(#GP)。雙四字的自然邊界是能夠被16整除的位址。其他的操作雙四字的指令允許未對齊的通路(不會産生通用保護異常),
然而,需要額外的記憶體總線周期來通路記憶體中未對齊的資料。
a) 以下在hp64機上驗證出資料對齊的結果
char ci=1 ci的位址是1 的倍數
short si=1 si的位址是2 的倍數
int ii=1 ii的位址是4 的倍數
long li=1 li的位址是8 的倍數
b) 以下是一個記憶體不對齊的例子:
unsigned char opc_2[]={0x40};
op1_1=(unsigned short *)opc_2; 可以給op1_1進行任何位址引用,如果不對齊,在引用值時發生錯誤緻命COREDUMP.
printf("op1_1=%p/n",*op1_1);
如果位址剛好是2的倍數,在以上能過執行,否則COREDUMP(在引用時産生printf).
說明:char型的位址因為是1的倍數,是以可能是2的倍數,4的倍數.....他是一個随機的.
如果剛好符合程式,如上轉化為
“指針的對齊”問題。
CPU一般要求指針的值(記憶體位址)要與它的指向類型資料的尺寸相比對。例如,2個位元組的資料類型被通路的位址值為 2 的倍數,
4個位元組的資料類型(如 int)被通路的位址值是 4 的倍數,等等。一個位元組的資料類型(如 char 型)對其通路位址無限制(因為是 1 的倍數)。
在Intel處理器上,指針對齊這個問題不是緻命的,至多占用CPU更多的時間進行指針轉換,進而帶來性能的下降;
但是對于其它類型的處理器來說就是緻命的了:如果通路的指針不對齊,會帶來運作錯誤。
當指針從一種類型的指針轉換到另外一種類型的指針的時候,就存在着産生非對齊指針的可能性。不過,正如原文中所說的,
“一個對齊要求較高的指針類型S轉換成一個對齊要求較低的指針類型D是安全的”,這種轉換不會産生非對齊指針;但是,
如果是一個對齊要求較低的指針類型D轉換成一個對齊要求較高的指針類型S,就有可能産生非對齊指針,這種轉換就是不安全。
<<c語言參考手冊>>中6.1.3節中提到,把一個對齊要求較高的指針類型S轉換成一個對齊要求較低的指針類型D是安全的,
安全指的是類型D在用于讀取與存儲D類型對象時可以得到預期效果,後面轉換回原指針類型時能夠恢複原指針.
舉例說明:
1) 較高的指針類型到較低的指針類型轉換
在unix上,每個int型資料占4個位元組,在hp系統上,例如,十六進制表示的整數 0x1a2b3c4d 在記憶體中是這樣存放的:
(高存儲位址)
Base Address +0 1a
Base Address +1 2b
Base Address +2 3c
Base Address +3 4d
(低存儲位址)
如果有這樣的程式:
代碼:
int a = 0x1a2b3c4d;
int *p = &a;
char *q = (char *)p;
printf("%p/n", *q); //執行結果:000000000000001a
p=NULL;
p = (int*)q;
printf("%p/n", *p); //執行結果:000000001a2b3c4d
則其中的指針 p 和 q 的值就是 Base Address。q 是char型指針(重要),是以 *q 的結果得到 0x1a不就是我們期待的嗎?
在這種情況下,不會得到 0x1a 以外的值,是以是也可以說是“安全”的。
*****重要:搞清楚指針操作受指針“基類型”而不是指針“所指向的對象類型”支配 就沒問題了****
無論對于自己的程式還是系統來說。程式後面兩句說明,從 q 指針能夠恢複原來的 p 指針,從結果來看也能得到我們預期的值。
1) 較低的指針類型到較高的指針類型轉換
把一個對齊要求較低的指針類型D轉換成一個對齊要求較高的指針類型S是不安全的,得不到預期值。例如,char* 到 int*的轉換:
代碼:
char c = 0x1a;
char* p = &c;
int* q = (int*)p;
printf("%p/n", *q); //執行結果:000000001a000000
p=NULL;
p = (char*)q;
printf("%p/n", *p); //執行結果:000000000000001a
你無法預測(預期)列印出的 *q 的值是什麼,因為我們除了知道整數的一個低位位元組(0x1a)之外,對于這個位元組後面的其餘三個位元組位一無所知,
其值是不确定的。是以,這樣的指針轉換就不是安全的(更嚴重的情況是用 *q 寫資料,會破壞掉 0x1a 後面的三個位元組的資料,給程式帶來錯誤隐患),
其結果也是不能預測的。通過 q 恢複原來的指針 p 沒有問題。
另外說明:
對于char型數組可以自然對其
例如: char[9],位址是8的倍數 可以把它的值賦給LONG型資料。
char[5],位址是4的倍數
char[3],位址是2的倍數