結構體成員的記憶體分佈與對齊
我們先看一道IBM和微軟的筆試題:
IBM筆試題:
struct{
short a1;
short a2;
short a3;
}A;
struct{
long a1;
short a2;
}B;
sizeof( A)=6, sizeof(B)=8,為什麼?
注:sizeof(short)=2,sizeof(long)=4
微軟筆試題:
struct example1
{
short a ;
long b;
};
struct example2
{
char c;
example1 struct1;
short e;
};
int main(int argc, char*argv[])
{
example2 e2;
int d=(unsigned int)&e2.struct1-(unsigned int)&e2.c;
printf("%d,%d,%d\n",sizeof(example1),sizeof(example2),d);
return 0;
}
輸出結果?
要能清除的分析上面的問題就要搞清楚結構體變數的成員在記憶體裡是如何分佈的、成員先後順序是怎樣的、成員之間是連續的還是分散的、還是其他的什麼形式?其實這些問題既和軟體相關又和硬體相關。所謂軟體相關主要是指和具體的程式語言的編譯器的特性相關,編譯器為了優化CPU訪問記憶體的效率,在生成結構體成員的起始地址時遵循著某種特定的規則,這就是所謂的 結構體成員“對齊”;所謂硬體相關主要是指CPU的“位元組序”問題,也就是大於一個位元組型別的資料如int型別、short型別等,在記憶體中的存放順序,即單個位元組與高低地址的對應關係。位元組序分為兩類:Big-Endian和Little-Endian,有的文章上稱之為“大端”和“小端”,他們是這樣定義的:
Little-Endian就是低位位元組排放在記憶體的低地址端,高位位元組排放在記憶體的高地址端;Big-Endian就是高位位元組排放在記憶體的低地址端,低位位元組排放在記憶體的高地址端。
Intel、VAX和Unisys處理器的計算機中的資料的位元組順序是Little-Endian,IBM 大型機和大多數Unix平臺的計算機中位元組順序是Big –Endian。
關與Big-Endian和Little-Endian問題本文暫不做詳細討論,本文將以小端機(此處為intel x86架構的計算機)、OS:WindowsXp和VC++6.0編譯器來詳細討論結構體成員的“對齊”問題。
前面說了,為了優化CPU訪問記憶體的效率,程式語言的編譯器在做變數的儲存分配時就進行了分配優化處理,優化規則大致原則是這樣:
對於n位元組的元素(n=2,4,8,...),它的首地址能被n整除,這種原則稱為“對齊”,如WORD(2位元組)的值應該能被2整除的位置,DWORD(4位元組)應該在能被4整除的位置。
對於結構體來說,結構體的成員在記憶體中順序存放,所佔記憶體地址依次 增高,第一個成員處於低地址處,最後一個成員處於最高地址處,但結構體成員的記憶體分配不一定是連續的,編譯器會對其成員變數依據前面介紹的 “對齊”原則進行處理。對待每個成員類似於對待單個n位元組的元素一樣,依次為每個元素找一個適合的首地址,使得其符合上述的“對齊”原則。通常編譯器中可以設定一個對齊引數n,但這個n並不是結構體成員實際的對齊引數,VC++6.0中結構體的每個成員實際對齊引數N通常是這樣計算得到的N=min(sizeof(該成員型別),n)(n為VC++6.0中可設定的值)。
成員的記憶體分配規律是這樣的:從結構體的首地址開始向後依次為每個成員尋找第一個滿足條件的首地址x,該條件是x % N = 0,並且整個結構的長度必須為各個成員所使用的對齊引數中最大的那個值的最小整數倍,不夠就補空位元組。
結構體中所有成員的對齊引數N的最大值稱為結構體的對齊引數。
VC++6.0中n預設是8個位元組,可以修改這個設定的對齊引數,方法為在選單“工程”的“設定”中的“C/C++”選項卡的“分類”中 “CodeGeneration ”的“Struct member alignment” 中設定,1byte、2byte、4byte、8byte、16byte等幾種,預設為8byte
也可以程式控制,採用指令:#pragma pack(xx)控制
如#pragma pack(1),1位元組對齊,#pragma pack(4),4位元組對齊
#pragma pack(16),16位元組對齊
接下來我們將分不同的情況來詳細討論結構體成員的分佈情況,順便提醒一下,
常見型別的長度:
Int 4byte,
Short 2byte,
Char 1byte,
Double 8byte,
Long 4byte
讓我們先看下例:
struct A
{
char c; //1byte
double d; //8byte
short s; //2byte
int i; //4byte
};
int main(int argc, char*argv[])
{
A strua;
printf("%len:d\n",sizeof(A));
printf("%d,%d,%d,%d",&strua.c,&strua.d,&strua.s,&strua.i);
return 0;
}
1)n設定為8byte時
結果:len:24,
1245032,1245040,1245048,1245052
記憶體中成員分佈如下:
strua.c分配在一個起始於8的整數倍的地址1245032(為什麼是這樣讀者先自己思考,讀完就會明白),接下來要在strua.c之後分配strua.d,由於double為8位元組,取N=min(8,8),8位元組來對齊,所以從strua.c向後找第一個能被8整除的地址,所以取1245032+8得1245040, strua.s 為2byte小於引數n,所以N=min(2,8),即N=2,取2位元組長度對齊,所以要從strua.d後面尋找第一個能被2整除的地址來儲存strua.s,由於strua.d後面的地址為1245048可以被2整除,所以strua.s緊接著分配,現在來分配strua.i,int為4byte,小於指定對齊引數8byte,所以N=min(4,8)取N=4byte對齊,strua.s後面第一個能被4整除地址為1245048+4,所以在1245048+4的位置分配了strua.i,中間補空,同時由於所有成員的N值的最大值為8,所以整個結構長度為8byte的最小整數倍,即取24byte其餘均補0.
於是該結構體的對齊引數就是8byte。
2)當對齊引數n設定為16byte時,結果同上,不再分析
3)當對齊引數設定為4byte時
上例結果為:Len:20
1245036,1245040,1245048,1245052
記憶體中成員分佈如下:
Strua.c起始於一個4的整數倍的地址,接下來要在strua.c之後分配strua.d,由於strua.d長度為8byte,大於對齊引數4byte,所以N=min(8,4)取最小的4位元組,所以向後找第一個能被4整除的地址來作為strua.d首地址,故取1245036+4,接著要在strua.d後分配strua.s,strua.s長度為2byte小於4byte,取N=min(2,4)2byte對齊,由於strua.d後的地址為1245048可以被2
整除,所以直接在strua.d後面分配,strua.i的長度為4byte,所以取N=min(4,4)4byte對齊,所以從strua.s向後找第一個能被4整除的位置即1245048+4來分配和strua.i,同時N的最大值為4byte,所以整個結構的長度為4byte的最小整數倍16byte
4)當對齊引數設定為2byte時
上例結果為:Len:16
1245040,1245042,1245050,1245052
Strua.c分配後,向後找一第一個能被2整除的位置來存放strua.d,依次類推
5)1byte對齊時:
上例結果為:Len:15
1245040,1245041,1245049,1245051
此時,N=min(sizeof(成員),1),取N=1,由於1可以整除任何整數,所以各個成員依次分配,沒有間空,如下圖所示:
6)當結構體成員為陣列時,並不是將整個陣列當成一個成員來對待,而是將陣列的每個元素當一個成員來分配,其他分配規則不變,如將上例的結構體改為:
struct A
{
char c; //1byte
double d; //8byte
short s; //2byte
char szBuf[5];
};
對齊引數設定為8byte,則,執行結果如下:
Len:24
1245032,1245040,1245048,1245050
Strua 的s分配後,接下來分配Strua 的陣列szBuf[5],這裡要單獨分配它的每個元素,由於是char型別,所以N=min(1,8),取N=1,所以陣列szBuf[5]的元素依次分配沒有間隙。
7)當結構中有成員不是一個完整的型別單元,如int或short型,而是該型別的一段時,即位段時,如
struct A
{
int a1:5;
int a2:9;
char c;
int b:4;
short s;
};
對於位段成員,儲存是按其型別分配空間的,如int 型就分配4個連續的儲存單元,如果是相鄰的同類型的段位成員就連續存放,共用儲存單元,此處如a1,a2將公用一個4位元組的儲存單元,當該型別的長度不夠用時,就另起一個該型別長度的儲存空間。有位段時的對齊規則是這樣:同類型的、相鄰的可連續在一個型別的儲存空間中存放的位段成員作為一個該型別的成員變數來對待,不是同類型的、相鄰的位段成員,分別當作一個單獨得該型別的成員來對待,分配一個完整的型別空間,其長度為該型別的長度,其他成員的分配規則不變,仍然按照前述的對齊規則進行。
對於 struct A,VC++6.0中n設定為8時,sizeof(A)=16,記憶體分佈:
又如:
struct B
{
int a:5;
int b:7;
int c:6;
int d:9;
char e:2;
int x;
};
Vc++6.0的對齊引數設定為8、16、4位元組對齊時,sizeof(A)=12記憶體分佈為:
(灰色部分未使用)
當對齊引數設定為2位元組時:(灰色部分未使用)sizeof(A)=10
又如intel的筆試題:
#include “stdafx.h”
#include <iostream.h>
struct bit
{
int a:3;
int b:2;
int c:3;
};
int main(int argc, char* argv[])
{
bit s;
char *c = (char*)&s;
*c = 0x99;
cout<<s.a<<endl<<s.b<<endl<<s.c<<endl;
return 0;
}
Output:?
執行的結果是 1 -1 -4
結構bit的成員在記憶體中由低地址到高地址順序存放,執行*c=0x99;後成員的記憶體分佈情況為:
8)當結構體成員是結構體型別時,那麼該過程是個遞迴過程,且把該成員作為一個整體來對待,如(微軟筆試題):
struct example1
{
short a ;
long b;
};
struct example2
{
char c;
example1 struct1;
short e;
};
int main(int argc, char*argv[])
{
example2 e2;
int d=(unsigned int)&e2.struct1-(unsigned int)&e2.c;
printf("%d,%d,%d\n",sizeof(example1),sizeof(example2),d);
return 0;
}
8byte對齊時,結果為:
8,16,4
記憶體分佈為:
因為example1的對齊引數為4,分配完c後要接著分配struct1,這時的對齊引數為min(struct1的對齊引數,指定對齊引數),開始分配struct1,在struct1的成員分配過程中又是按照前述的規則來分配的。
記憶體對齊”應該是編譯器的“管轄範圍”。編譯器為程式中的每個“資料單元”安排在適當的位置上。但是C語言的一個特點就是太靈活,太強大,它允許你干預“記憶體對齊”。如果你想了解更加底層的祕密,“記憶體對齊”對你就不應該再透明瞭。
一、記憶體對齊的原因
大部分的參考資料都是如是說的:
1、平臺原因(移植原因):不是所有的硬體平臺都能訪問任意地址上的任意資料的;某些硬體平臺只能在某些地址處取某些特定型別的資料,否則丟擲硬體異常。
2、效能原因:資料結構(尤其是棧)應該儘可能地在自然邊界上對齊。原因在於,為了訪問未對齊的記憶體,處理器需要作兩次記憶體訪問;而對齊的記憶體訪問僅需要一次訪問。
二、對齊規則
每個特定平臺上的編譯器都有自己的預設“對齊係數”(也叫對齊模數)。程式設計師可以通過預編譯命令#pragmapack(n),n=1,2,4,8,16來改變這一系數,其中的n就是你要指定的“對齊係數”。
對齊步驟:
1、資料成員對齊規則:結構(struct)(或聯合(union))的資料成員,第一個資料成員放在offset為0的地方,以後每個資料成員的對齊按照#pragmapack指定的數值和這個資料成員自身長度中,比較小的那個進行。
2、結構(或聯合)的整體對齊規則:在資料成員完成各自對齊之後,結構(或聯合)本身也要進行對齊,對齊將按照#pragma pack指定的數值和結構(或聯合)最大資料成員長度中,比較小的那個進行。
3、結合1、2顆推斷:當#pragma pack的n值等於或超過所有資料成員長度的時候,這個n值的大小將不產生任何效果。
備註:陣列成員按長度按陣列型別長度計算,如char t[9],在第1步中資料自身長度按1算,累加結構體時長度為9;第2步中,找最大資料長度時,如果結構體T有複雜型別成員A的,該A成員的長度為該複雜型別成員A的最大成員長度。
三、試驗
我們通過一系列例子的詳細說明來證明這個規則吧!
我試驗用的編譯器包括GCC3.4.2和VC6.0的C編譯器,平臺為Windows XP + Sp2。
我們將用典型的struct對齊來說明。首先我們定義一個struct:
#pragma pack(n) /* n = 1, 2, 4, 8, 16 */
struct test_t {
int a;
char b;
short c;
char d;
};
#pragma pack(n)
首先我們首先確認在試驗平臺上的各個型別的size,經驗證兩個編譯器的輸出均為:
sizeof(char) = 1
sizeof(short) = 2
sizeof(int) = 4
我們的試驗過程如下:通過#pragmapack(n)改變“對齊係數”,然後察看sizeof(structtest_t)的值。
1、1位元組對齊(#pragma pack(1))
輸出結果:sizeof(structtest_t) = 8 [兩個編譯器輸出一致]
分析過程:
1) 成員資料對齊
#pragma pack(1)
struct test_t {
int a; /* 長度4< 1 按1對齊;起始offset=0 0%1=0;存放位置區間[0,3] */
char b; /* 長度1= 1 按1對齊;起始offset=4 4%1=0;存放位置區間[4] */
short c; /* 長度2> 1 按1對齊;起始offset=5 5%1=0;存放位置區間[5,6] */
char d; /* 長度1= 1 按1對齊;起始offset=7 7%1=0;存放位置區間[7] */
};
#pragma pack()
成員總大小=8
2) 整體對齊
整體對齊係數 =min((max(int,short,char), 1) = 1
整體大小(size)=$(成員總大小) 按 $(整體對齊係數) 圓整 = 8 /* 8%1=0 */ [注1]
2、2位元組對齊(#pragma pack(2))
輸出結果:sizeof(structtest_t) = 10 [兩個編譯器輸出一致]
分析過程:
1) 成員資料對齊
#pragma pack(2)
struct test_t {
int a; /* 長度4> 2 按2對齊;起始offset=0 0%2=0;存放位置區間[0,3] */
char b; /* 長度1< 2 按1對齊;起始offset=4 4%1=0;存放位置區間[4] */
short c; /* 長度2= 2 按2對齊;起始offset=6 6%2=0;存放位置區間[6,7] */
char d; /* 長度1< 2 按1對齊;起始offset=8 8%1=0;存放位置區間[8] */
};
#pragma pack()
成員總大小=9
2) 整體對齊
整體對齊係數 =min((max(int,short,char), 2) = 2
整體大小(size)=$(成員總大小) 按 $(整體對齊係數) 圓整 = 10 /* 10%2=0 */
3、4位元組對齊(#pragma pack(4))
輸出結果:sizeof(structtest_t) = 12 [兩個編譯器輸出一致]
分析過程:
1) 成員資料對齊
#pragma pack(4)
struct test_t {
int a; /* 長度4= 4 按4對齊;起始offset=0 0%4=0;存放位置區間[0,3] */
char b; /* 長度1< 4 按1對齊;起始offset=4 4%1=0;存放位置區間[4] */
short c; /* 長度2< 4 按2對齊;起始offset=6 6%2=0;存放位置區間[6,7] */
char d; /* 長度1< 4 按1對齊;起始offset=8 8%1=0;存放位置區間[8] */
};
#pragma pack()
成員總大小=9
2) 整體對齊
整體對齊係數 =min((max(int,short,char), 4) = 4
整體大小(size)=$(成員總大小) 按 $(整體對齊係數) 圓整 = 12 /* 12%4=0 */
4、8位元組對齊(#pragma pack(8))
輸出結果:sizeof(structtest_t) = 12 [兩個編譯器輸出一致]
分析過程:
1) 成員資料對齊
#pragma pack(8)
struct test_t {
int a; /* 長度4< 8 按4對齊;起始offset=0 0%4=0;存放位置區間[0,3] */
char b; /* 長度1< 8 按1對齊;起始offset=4 4%1=0;存放位置區間[4] */
short c; /* 長度2< 8 按2對齊;起始offset=6 6%2=0;存放位置區間[6,7] */
char d; /* 長度1< 8 按1對齊;起始offset=8 8%1=0;存放位置區間[8] */
};
#pragma pack()
成員總大小=9
2) 整體對齊
整體對齊係數 =min((max(int,short,char), 8) = 4
整體大小(size)=$(成員總大小) 按 $(整體對齊係數) 圓整 = 12 /* 12%4=0 */
5、16位元組對齊(#pragma pack(16))
輸出結果:sizeof(structtest_t) = 12 [兩個編譯器輸出一致]
分析過程:
1) 成員資料對齊
#pragma pack(16)
struct test_t {
int a; /* 長度4< 16 按4對齊;起始offset=0 0%4=0;存放位置區間[0,3] */
char b; /* 長度1< 16 按1對齊;起始offset=4 4%1=0;存放位置區間[4] */
short c; /* 長度2< 16 按2對齊;起始offset=6 6%2=0;存放位置區間[6,7] */
char d; /* 長度1< 16 按1對齊;起始offset=8 8%1=0;存放位置區間[8] */
};
#pragma pack()
成員總大小=9
2) 整體對齊
整體對齊係數 =min((max(int,short,char), 16) = 4
整體大小(size)=$(成員總大小) 按 $(整體對齊係數) 圓整 = 12 /* 12%4=0 */
記錄型別的記憶體分配!
Packed Record和Record的不同之處!
type
MyRec=Record
var1:integer;
var2,var3,var4,var5,var6,var7,var8:shortint;
var9:integer;
var10:shortint;
var11:integer;
var12,var13:shortint;
end;
...
ShowMessage(intTostr(SizeOf(MyRec)));
結果顯示為18,而按我想象應為16。請高手講解一下Delphi5.0中變數記憶體空間分配機制,因為我有一個數組MyArray:Array[1..1000000]of MyRec;需要考慮節省記憶體問題,
另外不要說我懶不愛看書,我手頭所有關於Delphi的書都沒有提到這個問題。
回答:
顯示的結果應該為28,而不是18!按道理應該是22。用Packed的結果就是22。
擬定義的陣列比較大,應該用packedrecord!
原因如下:
在Windows中記憶體的分配一次是4個位元組的。而Packed按位元組進行記憶體的申請和分配,這樣速度要慢一些,因為需要額外的時間來進行指標的定位。因此如果不用Packed的話,Delphi將按一次4個位元組的方式申請記憶體,因此如果一個變數沒有4個位元組寬的話也要佔4個位元組!這樣就浪費了。按上面的例子來說:
var1:integer;//integer剛好4個位元組!
var2-var5佔用4個位元組,Var6-Var8佔用4個位元組,浪費了一個位元組。
var9:integer//佔用4個位元組;
var10:佔用4個位元組;浪費3個位元組
var11:佔用4個位元組;
var12,var13佔用4個位元組;浪費2個位元組
所以,如果不用packed的話,那麼一共浪費6個位元組!所以原來22個位元組的記錄需要28個位元組的記憶體空間!
****************
回覆人:eDRIVE(eDRIVE) (2001-3-2 17:45:00) 得0分
這是因為在32位的環境中,所有變數分配的記憶體都進行“邊界對齊”造成的。這樣做可以對速度有優化作用;但是單個定義的變數至少會佔用32位,即4個位元組。所以會有長度誤差,你可以用packed關鍵字取消這種優化。
深入的分析,記憶體空間(不是記憶體地址)在計算機中劃分為無數與匯流排寬度一致的單位,單位之間相接的地方稱為“邊界”;匯流排在對記憶體進行訪問時,每次訪問週期只能讀寫一個單位(32bit),如果一個變數橫跨“邊界”的話,則讀或寫這個變數就得用兩個訪問週期,而“邊界對齊”時,只需一個訪問週期,速度當然會有所優化。
Record的資料各個位元組都是對齊的,資料格式比較完整,所以這種格式相對packed佔用的記憶體比較大,
但是因為格式比較整齊,所以電腦讀取這個型別的資料的時候速度比較快。
而PackedRecord對資料進行了壓縮,節省了記憶體空間,當然他的速度也變的慢了。
type
// Declare an unpacked record
TDefaultRecord = Record
name1 : string[4];
floater : single;
name2 : char;
int : Integer;
end;
// Declare a packed record
TPackedRecord = Packed Record
name1 : string[4];
floater : single;
name2 : char;
int : Integer;
end;
var
defaultRec : TDefaultRecord;
packedRec : TPackedRecord;
begin
ShowMessage('Default record size = '+IntToStr(SizeOf(defaultRec)));
ShowMessage('Packed record size = '+IntToStr(SizeOf(packedRec)));
end;
Default record size = 20
Packed record size = 14
不過,對於現在的作業系統來,packedRecord 節省的那些空間已不用考慮他了。除了做DLL(不用packed容易造成記憶體混亂)和做硬體程式設計時(比如串列埠)程式設計時必須用到packedRecord,其它情況都可以用Record
C的結構體與Delphi中的記錄型別 |
|
Object Pascal的指標 |