1. 程式人生 > >資料結構的記憶體分配、對齊,及指標加1的含義

資料結構的記憶體分配、對齊,及指標加1的含義

[1]    指標變數+1,代表著什麼?http://blog.csdn.net/bravekingzhang/article/details/6430590

[2]    結構體記憶體的空間分配原理,http://www.cnblogs.com/qintangtao/archive/2013/03/06/2945674.html

本篇轉載文章,我綜合了以上三篇文獻,並進行了梳理。

1  記憶體對齊及意義

記憶體地址對齊,是一種在計算機記憶體中排列資料(表現為變數的地址)、訪問資料(表現為CPU讀取資料)的一種方式,包含了兩種相互獨立又相互關聯的部分:基本資料對齊和結構體資料對齊。

為什麼需要記憶體對齊?對齊有什麼好處?是我們程式設計師來手動做記憶體對齊呢?還是編譯器在進行自動優化的時候完成這項工作?

在現代計算機體系中,每次讀寫記憶體中資料,都是按字(word,4個位元組,對於X86架構,系統是32位,資料匯流排和地址匯流排的寬度都是32位,所以最大的定址空間為232 = 4GB(也許有人會問,我的32位XP用不了4GB記憶體,關於這個不在本篇博文討論範圍),按A[31,30…2,1,0]這樣排列,但是請注意為了CPU每次讀寫 4個位元組定址,A[0]和A[1]兩位是不參與定址計算的。)為一個塊(chunks)來操作(而對於X64則是8個位元組為一個塊)。注意,這裡說的 CPU每次讀取的規則,並不是變數在記憶體中地址對齊規則。

既然是這樣的,如果變數在記憶體中儲存的時候也按照這樣的對齊規則,就可以加快CPU讀寫記憶體的速度,當然也就提高了整個程式的效能,並且效能提升是客觀,雖然當今的CPU的處理資料速度(是指邏輯運算等,不包括取址)遠比記憶體訪問的速度快,程式的執行速度的瓶頸往往不是CPU的處理速度不夠,而是記憶體訪問的延遲,雖然當今CPU中加入了快取記憶體用來掩蓋記憶體訪問的延遲,但是如果高密集的記憶體訪問,這種延遲是無可避免的,記憶體地址對齊會給程式帶來了很大的效能提升。

記憶體地址對齊是計算機語言自動進行的,也即是編譯器所做的工作。但這不意味著我們程式設計師不需要做任何事情,因為如果我們能夠遵循某些規則,可以讓編譯器做得更好,畢竟編譯器不是萬能的。

為了更好理解上面的意思,這裡給出一個示例。在32位系統中,假如一個int變數在記憶體中的地址是0x00ff42c3,因為int是佔用4個位元組,所以它的尾地址應該是0x00ff42c6,這個時候CPU為了讀取這個int變數的值,就需要先後讀取兩個word大小的塊,分別是0x00ff42c0~0x00ff42c3和0x00ff42c4~0x00ff42c7,然後通過移位等一系列的操作來得到,在這個計算的過程中還有可能引起一些匯流排資料錯誤的。但是如果編譯器對變數地址進行了對齊,比如放在0x00ff42c0,CPU就只需要一次就可以讀取到,這樣的話就加快讀取效率。

2  基本資料對齊

在X86,32位系統下基於Microsoft、Borland和GNU的編譯器,有如下資料對齊規則:

a)       一個char(佔用1-byte)變數以1-byte對齊。

b)       一個short(佔用2-byte)變數以2-byte對齊。

c)       一個int(佔用4-byte)變數以4-byte對齊。

d)       一個long(佔用4-byte)變數以4-byte對齊。

e)       一個float(佔用4-byte)變數以4-byte對齊。

f)        一個double(佔用8-byte)變數以8-byte對齊。

g)       一個long double(佔用12-byte)變數以4-byte對齊。

h)       任何pointer(佔用4-byte)變數以4-byte對齊。

而在64位系統下,與上面規則對比有如下不同:

a)       一個long(佔用8-byte)變數以8-byte對齊。

b)       一個double(佔用8-byte)變數以8-byte對齊。

c)       一個long double(佔用16-byte)變數以16-byte對齊。

d)       任何pointer(佔用8-byte)變數以8-byte對齊。

3  結構體資料對齊

結構體資料對齊,是指結構體內的各個資料對齊。在結構體中的第一個成員的首地址等於整個結構體的變數的首地址,而後的成員的地址隨著它宣告的順序和實際佔用的位元組數遞增。為了總的結構體大小對齊,會在結構體中插入一些沒有實際意思的字元來填充(padding)結構體。

結構體中成員資料對齊規則

1)       結構體中的第一個成員的首地址也即是結構體變數的首地址。

2)       結構體中的每一個成員的首地址相對於結構體的首地址的偏移量(offset),是根據該成員資料自己的對齊位元組數和PPB(指定的對齊位元組數,32位機預設為4)兩個位元組數最小的那個對齊。

3)       結構體的總大小是對齊模數(對齊模數等於#pragma pack(n)所指定的n與結構體中最大資料型別的成員大小的最小值)的整數倍。這樣在處理陣列時可以保證每一項都邊界對齊。

4)       對於結構體成員屬性中包含結構體變數的複合型結構體,在確定最寬基本型別成員時,應當包括複合型別成員的子成員。但在確定複合型別成員的偏移位置時則是將複合型別作為整體看待。

5)       還有一個額外的條件:結構體變數的首地址能夠被其最寬基本型別成員的大小所整除。(不太理解)

最主要的是前面三個規則。

總結出一個公式:結構體的大小等於最後一個成員的偏移量加上其大小再加上末尾的填充位元組數目,即:sizeof( struct ) = offsetof( last item ) + sizeof( last item )+sizeof( trailing padding )

示例一

struct

{

char a;

int b;

short c;

char d;

}dataAlign;

struct

{

char a;

char d;

short c;

int b;

}dataAlign2;

仔細觀察,會發現雖然是一樣的資料型別的成員,只不過宣告的順序不同,結構體佔用的大小也不同,一個8-byte一個12-byte。為什麼這樣,下面進行具體分析。 

首先來看dataAlign2

第一個成員的地址等於結構體變數的首地址,

第二個成員char型別,為了滿足規則2),它相對於結構體的首地址的偏移量必須是char=1的倍數,由於前面也是char,故不需要在第一個和第一個成員之間填充,直接滿足條件。

第三個成員short=2如果要滿足規則2),也不需要填充,因為它的偏移量已經是2。同樣第四個也因為偏移量int=4,不需要填充,這樣結構體總共大小為8-byte。

最後來驗證規則c,在VC中預設 的#pragma pack(n)中的n=8,而結構體中資料型別大小最大的為第四個成員int=4,故對齊模數為4,並且8 mode 4 = 0,所以滿足規則c。這樣整個結構體的總大小為8。

對於dataAlign

第一個成員等於結構體變數首地址,偏移量為0,

第二個成員為int=4,為了滿足規2),需要在第一個成員之後填充3-byte,讓它相對於結構體首地址偏移量為4,結合執行結果,可知&dataAlign.a = 0x01109140,而&dataAlign.b = 0x01109144,它們之間相隔4-byte,0x01109141~0x01109143三個位元組被0填充。

第三個成員short=2,無需填充滿足規則2)。

第四個成員char=1,也不需要填充。結構體總大小相加4 + 4 + 2 + 1 = 11。

同樣最後需要驗證規則c,結構體中資料型別大小最大為第二個成員int=4,比VC預設對齊模數8小,故這個結構體的對齊模數仍然為4,顯然11 mode 4 != 0,故為了滿足規則c,需要在char後面填充一個位元組,這樣結構體變數dataAlign的總大小為4 + 4 + 2 + 2 = 12。

示例二

#include

#pragma pack(2)  //指定PPB為2

struct T{

char a;     //偏移地址0

int b;           //偏移地址2

char c;     //偏移地址6

};

#pragma pack()   //恢復原來預設PPB,32位下為4

int main(int argc,char * argv[])

{

printf("sizeof(struct T));

return 0;

}

最後輸出的結果為:8。語句#pragma pack(2)的作用是指定結構體按2位元組對齊,即PPB=2。分析如下:

變數a預設為1位元組,PB=2,所以a按1位元組對齊,a的偏移地址為0。

變數b預設為4位元組(在32位機器中int為4位元組),PB=2,所以b按2位元組對齊,b的偏移地址為2。

變數c預設為1位元組,PB=2,所以c按1位元組對齊,偏移地址為6。

此時結構體的計算出的位元組數為7個位元組。最後按規則3,結構體對齊後的位元組數為8。sizeof(T)=6+1+1=8

注意的問題

1)       位元組對齊取決於編譯器;

2)       一定要注意PPB大小,PPB大小由pragam pack(n)指定;

3)       結構體佔用的位元組數要能被PPB整除。

4  關於指標變數 +1

指標的加法的實現,

如int * pofa,pofa++或著pofa=pofa+1,會轉換為pofa=pofa+sizeof(*pofa)*1,通過求指標指向變數的那個型別所佔的位元組數在乘指標偏移單位,然後做加法。

同時,對pofa=pofa+1後pofa所指的記憶體地址,仍然是按照pofa加1之前所指向的資料型別(int型)進行解讀。要向轉變解讀方式,則需要使用型別轉換,如(char*)(pofa+1)