【系統運維】內存對齊(二)

給你舉個例子,如下結構體成員的大小是a+b=1+4=5個字節,但是事實上結構體的大小是8,因爲要字節對齊,就是說32位的機器 
//它的每個地址是4個字節,那如果有零頭也會按4個字節算,這樣方便取地址,程序如下: 
#include <stdio.h> 
struct A{ 
char a; 
int b; 
};

int main (){ 
A ab; 
printf("sizeof(A)=%d,sizoeof(a)=%d,sizoe(b)=%d/n",sizeof(ab),sizeof(ab.a),sizeof(ab.b)); 
}

 

在沒有程序中自定義對齊方式的情況下編譯器一般默認對齊方式爲4字節對齊,那麼此結構體大小是 
12字節 
不是9字節,是因爲爲了內存對齊。 
之所以有內存對齊問題,我個人認爲是爲了提高程序執行效率。即犧牲空間節省時間。 
在這裏,具體內存分配情況如下: 
struct header 

BYTE by; //佔用1字節 
DWORD dw; //爲了內存對齊他從第5個字節開始存儲,並佔用4個 
int flag; //從第9個字節開始存儲,並佔用4個 
}; 
如果想強制只使用類型所佔空間的內存,那麼使用如下語句 
#pragma pack(1)

 

sizeof(結構體)和內存對齊 
Oct 4th, 2007 by king

有的時候,在腦海中停頓了很久的“顯而易見”的東西,其實根本上就是錯誤的。就拿下面的問題來看:

struct T 

char ch; 
int i ; 
}; 
使用sizeof(T),將得到什麼樣的答案呢?要是以前,想都不用想,在32位機中,int是4個字節,char是1個字節,所以T一共是5個字節。實踐出真知,在VC6中測試了下,答案確實8個字節。哎,反正受傷的總是我,我已經有點麻木了,還是老老實實的接受吧!爲什麼答案和自己想象的有出入呢?這裏將引入內存對齊這個概念。

許多實際的計算機系統對基本類型數據在內存中存放的位置有限制,它們會要求這些數據的首地址的值是某個數k(通常它爲4或8)的倍數,這就是所謂的內存對齊,而這個k則被稱爲該數據類型的對齊模數(alignment modulus)。當一種類型S的對齊模數與另一種類型T的對齊模數的比值是大於1的整數,我們就稱類型S的對齊要求比T強(嚴格),而稱T比S弱(寬鬆)。這種強制的要求一來簡化了處理器與內存之間傳輸系統的設計,二來可以提升讀取數據的速度。比如這麼一種處理器,它每次讀寫內存的時候都從某個8倍數的地址開始,一次讀出或寫入8個字節的數據,假如軟件能保證double類型的數據都從8倍數地址開始,那麼讀或寫一個double類型數據就只需要一次內存操作。否則,我們就可能需要兩次內存操作才能完成這個動作,因爲數據或許恰好橫跨在兩個符合對齊要求的8字節內存塊上。某些處理器在數據不滿足對齊要求的情況下可能會出錯,但是Intel的IA32架構的處理器則不管數據是否對齊都能正確工作。不過Intel奉勸大家,如果想提升性能,那麼所有的程序數據都應該儘可能地對齊。

ANSI C標準中並沒有規定,相鄰聲明的變量在內存中一定要相鄰。爲了程序的高效性,內存對齊問題由編譯器自行靈活處理,這樣導致相鄰的變量之間可能會有一些填充字節。對於基本數據類型(int char),他們佔用的內存空間在一個確定硬件系統下有個確定的值,所以,接下來我們只是考慮結構體成員內存分配情況。

Win32平臺下的微軟C編譯器(cl.exe for 80×86)的對齊策略: 
1) 結構體變量的首地址能夠被其最寬基本類型成員的大小所整除; 
備註:編譯器在給結構體開闢空間時,首先找到結構體中最寬的基本數據類型,然後尋找內存地址能被該基本數據類型所整除的位置,作爲結構體的首地址。將這個最寬的基本數據類型的大小作爲上面介紹的對齊模數。 
2) 結構體每個成員相對於結構體首地址的偏移量(offset)都是成員大小的整數倍,如有需要編譯器會在成員之間加上填充字節(internal adding); 
備註:爲結構體的一個成員開闢空間之前,編譯器首先檢查預開闢空間的首地址相對於結構體首地址的偏移是否是本成員的整數倍,若是,則存放本成員,反之,則在本成員和上一個成員之間填充一定的字節,以達到整數倍的要求,也就是將預開闢空間的首地址後移幾個字節。 
3) 結構體的總大小爲結構體最寬基本類型成員大小的整數倍,如有需要,編譯器會在最末一個成員之後加上填充字節(trailing padding)。 
備註:結構體總大小是包括填充字節,最後一個成員滿足上面兩條以外,還必須滿足第三條,否則就必須在最後填充幾個字節以達到本條要求。

根據以上準則,在windows下,使用VC編譯器,sizeof(T)的大小爲8個字節。

而在GNU GCC編譯器中,遵循的準則有些區別,對齊模數不是像上面所述的那樣,根據最寬的基本數據類型來定。在GCC中,對齊模數的準則是:對齊模數最大隻能是4,也就是說,即使結構體中有double類型,對齊模數還是4,所以對齊模數只能是1,2,4。而且在上述的三條中,第2條裏,offset必須是成員大小的整數倍,如果這個成員大小小於等於4則按照上述準則進行,但是如果大於4了,則結構體每個成員相對於結構體首地址的偏移量(offset)只能按照是4的整數倍來進行判斷是否添加填充。 
看如下例子:

struct T 

char ch; 
double d ; 
}; 
那麼在GCC下,sizeof(T)應該等於12個字節。

如果結構體中含有位域(bit-field),那麼VC中準則又要有所更改: 
1) 如果相鄰位域字段的類型相同,且其位寬之和小於類型的sizeof大小,則後面的字段將緊鄰前一個字段存儲,直到不能容納爲止; 
2) 如果相鄰位域字段的類型相同,但其位寬之和大於類型的sizeof大小,則後面的字段將從新的存儲單元開始,其偏移量爲其類型大小的整數倍; 
3) 如果相鄰的位域字段的類型不同,則各編譯器的具體實現有差異,VC6採取不壓縮方式(不同位域字段存放在不同的位域類型字節中),Dev-C++和GCC都採取壓縮方式; 
備註:當兩字段類型不一樣的時候,對於不壓縮方式,例如:

struct N 

char c:2; 
int i:4; 
}; 
依然要滿足不含位域結構體內存對齊準則第2條,i成員相對於結構體首地址的偏移應該是4的整數倍,所以c成員後要填充3個字節,然後再開闢4個字節的空間作爲int型,其中4位用來存放i,所以上面結構體在VC中所佔空間爲8個字節;而對於採用壓縮方式的編譯器來說,遵循不含位域結構體內存對齊準則第2條,不同的是,如果填充的3個字節能容納後面成員的位,則壓縮到填充字節中,不能容納,則要單獨開闢空間,所以上面結構體N在GCC或者Dev-C++中所佔空間應該是4個字節。

4) 如果位域字段之間穿插着非位域字段,則不進行壓縮; 
備註: 
結構體

typedef struct 

char c:2; 
double i; 
int c2:4; 
}N3; 
在GCC下佔據的空間爲16字節,在VC下佔據的空間應該是24個字節。 
5) 整個結構體的總大小爲最寬基本類型成員大小的整數倍。

ps:

對齊模數的選擇只能是根據基本數據類型,所以對於結構體中嵌套結構體,只能考慮其拆分的基本數據類型。而對於對齊準則中的第2條,確是要將整個結構體看成是一個成員,成員大小按照該結構體根據對齊準則判斷所得的大小。 
類對象在內存中存放的方式和結構體類似,這裏就不再說明。需要指出的是,類對象的大小隻是包括類中非靜態成員變量所佔的空間,如果有虛函數,那麼再另外增加一個指針所佔的空間即可。

 


關於內存對齊問題.
懸賞分:0 - 解決時間:2007-7-30 06:02
沒弄明白內存對齊的方式. 
#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() 
爲什麼c要佔用6,7單元而不是5,6單元呢.
問題補充:那是怎麼保證第一個元素的內存地址都是在0位置的呢?
提問者: wm09323 - 魔法師 四級 最佳答案
pack(2) 就是要保證變量在2字節邊界對齊,即每個變量的起始地址是2的倍數 
b存放在區間[4] ,區間〔5〕閒置不用

以此類推了,pack(4)就是4字節對齊。。。。

 

 

看錯, 
#pragma pack(n)的作用你還沒理解透

這個n的值可以爲1, 2, 4, 8, 16,pack的原則是儘量使其在內存中和機器的自然字長相同,你的機器自然字長可能是2字也就是4字節,所以當n > 4時,字節數小於4的類型會被自動填充爲4字節,大於4字節的才根據n的指定來填充,所以你那個類的內存排列方式應該是這樣:

vptr: 4, int 4, double 16;

我考慮過是否是編譯器優化的結果,但我看了下發現我的編譯器優化是被關掉的,而且struct和clas的對齊方式是默認default,所以得出以上結論,僅供參考!

 


受你問題的啓發,這幾研究了下計算機組成原理,其中看了一本書叫Write Great Code Vol1——Understanding The Machine,在內存結構那章有這麼一段:

Figure 7-8 also suggests that compilers pack the fields into adjacent memory locations with no gaps between the fields. While this is true for many languages, this certainly isn't the most common memory organization for a record. For performance reasons, most compilers will actually align the fields of a record on appropriate memory boundaries. The exact details vary by language, compiler implementation, and CPU, but a typical compiler will place fields at an offset within the record's storage area that is 'natural' for that particular field's data type. On the 80x86, for example, compilers that follow the Intel ABI (application binary interface) will allocate one-byte objects at any offset within the record, words only at even offsets, and double-word or larger objects on double-word boundaries. Although not all 80x86 compilers support the Intel ABI, most do, which allows records to be shared among functions and procedures written in different languages on the 80x86. Other CPU manufacturers provide their own ABI for their processors and programs that adhere to an ABI can share binary data at run time with other programs that adhere to the same ABI.

說的是關於結構體中數據的對齊方式因語言、編譯器和CPU而異,但是通常的做法就是把數據成員放置在與該成員的類型較爲符合(也稱自然放置)的地方。

因爲Intel的 Pentium 系列處理器屬於x86家族,x86家族中:

8088, 8086, 80186, 80188 只有20條數據總線,尋址能力爲1M

80286, 80386sx 有24條數據總線 可尋址16M

80386dx 有32條數據總線 可尋址4GB

80486, Pentium 也是32條 尋址4GB

Pentium Pro, II, III, IV 有36條數據總線 可尋址64GB

P4應該屬於Prntium Pro,有36條數據總線,我的也是P42.4G,但x86家族的通用整型寄存器是32位的,也就是在每個CPU時鐘週期中最多這能處理32位數據。

一般所說的32,64位CPU都指的是其數據總線數量,而不是CPU寄存器所以P4是屬於32位的處理器(以後買CPU要看寄存器大小不要被表面的32, 64云云,給蒙啦...)

所以Pentium Pro多出來的那4條總線P用沒有!

因爲有32條能用到的數據總線,所以每個cpu指令可操作的對象最大可能就爲32bit,即8個位,x86家族採用2個memory bank表示一個字,所以32位數據總線可由4個bank來表示,所以其內部內存模型爲: 
QW: quad-word

.^ 
.| 
.| 
QWX2: XX XX XX XX // 從左往右依次是bank 1, 2, 3, 4 
QWX1: XX XX XX XX // 8位 X表數據未知 
QWX0: XX XX XX XX // 8位 bank中的每個X表示一個位 
.| 
.|地址由下往上增 

// 右邊的數字每一個表示一個位

而x86CPU對打包數據體(結構,聯合,這裏聯合被排除)中的數據類型的排列方式是遵循以下原則的:

1、Compiler will allocate one-byte objects at any offset within the record.

編譯器將在結構基地址的任意偏移量處分配1字節(這就是爲什麼創建一個空類也佔1字節的原因)。

2、Words only at even offsets。

字只處在偏移量爲偶數的地方。

3、Double-word or larger objects on double-word boundaries.

雙字或更大的數據體只處在雙字的邊緣上。

以上是我看到的一點心得,應該是Intel處理器和大部分編譯器的默認處理方式,給你分享下。

另外我又試驗了以後發現竟然是VC的問題,我在除VC外的其他幾個編譯器上試驗的結果都是16位,對齊原則和我跟你說的一樣,VC爲什麼是24呢?因爲它編譯器裏的默認對齊方式是8字節,請看以下結果:

#include <iostream> 
using namespace std;

//#pragma pack()

class dd{ 
public: 
int a; 
double c; 
virtual ~dd(){} 
}a, b;

int main() 

cout << &a << endl; 
cout << &a.a << endl; 
cout << &a.c << endl; 
cout << &b; 
}


運行結果是

0041E008 // vptr 
0041E010 // int 
0041E018 // double 
0041E020

內存模型可能就是這樣: 
X: 填充字節

.^ 
.| 
.| 
QWX2: HH HH HH HH // double 
QWX1: XX XX BB BB // int 
QWX0: XX XX AA AA // vptr 
.| 
.|地址由下往上增 
.| 
所以是3*8 = 24字節

可見編譯器把vptr也分配爲了8字節,可能有4字節的填充,原因我想很簡單,就是VC迎合32位寄存器的CPU的結果,因爲這樣就可以每次讀取的最大32位數據都可以在一個32位的通用整型寄存器中來執行操作,只需要一個指令就可以把數據分離出來,而如果是以4字節對齊的話,就需要2個指令,先把寄存器中的高16位(由於intel是反着存的)讀入16位通用整型寄存器(AX,BX,CX,DX),再把數據提取出來。 
這樣的結果是增加了程序執行的內存開銷,但提高了執行速度,典型的犧牲空間換取時間的做法(貌似現在都以時間爲優先考慮,計算機的RAM是越來越大,就連cache都有好幾級大到數M了-_-~)。


但在其他的編譯器上產生的結果就是16,總之#pragma 宏的實現本來就是最不可靠的,因爲很大程度上它的實現是取決與編譯器和CPU,所以遇到這樣的問題還是要結合多方面考慮,另外多吸收一些計算機結構的底層知識也能幫助自己的理解(說“幫助”有點牽強,因爲了解了底層實現那還有什麼高級“花招”能忽悠得了我的^_^)。

PS: 多謝你的這個問題啊呵呵,我這段時間讀了3本計算機結構的書,對底層有了一定了解,今後打算把Intel的3本Developer manual也搞定,哈哈過癮!!

 

 

華爲的比試題,內存對齊,請教高手詳解
懸賞分:5 - 解決時間:2008-11-7 16:40
還是看個例子把,華爲的比試題 
struct tagAAA 

unsigned char ucld:1; 
unsigned char ucpara0:2; 
unsigned char ucstate:6; 
unsigned char uctail:4; 
unsigned char ucavail; 
unsigned char uctail2:4; 
unsigned long uldate; 
}AAA_s; 
問:AAA_s在字節對齊分別爲1,4的情況下佔用空間大小是多少?還有那個冒號是什麼意思啊
提問者: xingdabo5921 - 初入江湖 三級 最佳答案
位域

有些信息在存儲時,並不需要佔用一個完整的字節, 而只需佔幾個或一個二進制位。例如在存放一個開關量時,只有0和1 兩種狀態, 用一位二進位即可。爲了節省存儲空間,並使處理簡便,C語言又提供了一種數據結構,稱爲“位域”或“位段”。所謂“位域”是把一個字節中的二進位劃分爲幾個不同的區域, 並說明每個區域的位數。每個域有一個域名,允許在程序中按域名進行操作。 這樣就可以把幾個不同的對象用一個字節的二進制位域來表示。一、位域的定義和位域變量的說明位域定義與結構定義相仿,其形式爲:

struct 位域結構名

{ 位域列表 };

其中位域列表的形式爲: 類型說明符 位域名:位域長度

例如:

struct bs 

int a:8; 
int b:2; 
int c:6; 
};

位域變量的說明與結構變量說明的方式相同。 可採用先定義後說明,同時定義說明或者直接說明這三種方式。例如:

struct bs 

int a:8; 
int b:2; 
int c:6; 
}data;

說明data爲bs變量,共佔兩個字節。其中位域a佔8位,位域b佔2位,位域c佔6位。對於位域的定義尚有以下幾點說明:

1. 一個位域必須存儲在同一個字節中,不能跨兩個字節。如一個字節所剩空間不夠存放另一位域時,應從下一單元起存放該位域。也可以有意使某位域從下一單元開始。例如:

struct bs 

unsigned a:4 
unsigned :0 /*空域*/ 
unsigned b:4 /*從下一單元開始存放*/ 
unsigned c:4 
}

在這個位域定義中,a佔第一字節的4位,後4位填0表示不使用,b從第二字節開始,佔用4位,c佔用4位。

2. 由於位域不允許跨兩個字節,因此位域的長度不能大於一個字節的長度,也就是說不能超過8位二進位。

3. 位域可以無位域名,這時它只用來作填充或調整位置。無名的位域是不能使用的。例如:

struct k 

int a:1 
int :2 /*該2位不能使用*/ 
int b:3 
int c:2 
};

從以上分析可以看出,位域在本質上就是一種結構類型, 不過其成員是按二進位分配的。

二、位域的使用

位域的使用和結構成員的使用相同,其一般形式爲: 位域變量名·位域名 位域允許用各種格式輸出。

main(){ 
struct bs 

unsigned a:1; 
unsigned b:3; 
unsigned c:4; 
} bit,*pbit; 
bit.a=1; 
bit.b=7; 
bit.c=15; 
printf("%d,%d,%d/n",bit.a,bit.b,bit.c); 
pbit=&bit; 
pbit->a=0; 
pbit->b&=3; 
pbit->c|=1; 
printf("%d,%d,%d/n",pbit->a,pbit->b,pbit->c); 
}

上例程序中定義了位域結構bs,三個位域爲a,b,c。說明了bs類型的變量bit和指向bs類型的指針變量pbit。這表示位域也是可以使用指針的。

程序的9、10、11三行分別給三個位域賦值。( 應注意賦值不能超過該位域的允許範圍)程序第12行以整型量格式輸出三個域的內容。第13行把位域變量bit的地址送給指針變量pbit。第14行用指針方式給位域a重新賦值,賦爲0。第15行使用了複合的位運算符"&=", 該行相當於: pbit->b=pbit->b&3位域b中原有值爲7,與3作按位與運算的結果爲3(111&011=011,十進制值爲3)。同樣,程序第16行中使用了複合位運算"|=", 相當於: pbit->c=pbit->c|1其結果爲15。程序第17行用指針方式輸出了這三個域的值。

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