這也可以?
複製代碼 代碼如下:
#include <iostream>
using namespace std;
struct Test_A
{
char a;
char b;
int c;
};
struct Test_B
{
char a;
int c;
char b;
};
struct Test_C
{
int c;
char a;
char b;
};
int main()
{
struct Test_A a;
memset(&a, 0, sizeof(a));
struct Test_B b;
memset(&b, 0, sizeof(b));
struct Test_C c;
memset(&c, 0, sizeof(c));
// Print the memory size of the struct
cout<<sizeof(a)<<endl;
cout<<sizeof(b)<<endl;
cout<<sizeof(c)<<endl;
return 0;
}
好了,一段簡單的程式,上面的這段程式輸出是什嗎?如果你很懂,也就會知道我接下來要講什麼了,可以略過了;如果,你不知道,或者還很模糊,請繼續閱讀。
這是為什嗎?
上面這段程式的輸出結果如下(windows 8.1 + visual studio 2012 update3下運行):
複製代碼 代碼如下:
// Print the memory size of the struct
cout<< sizeof(a)<<endl; // 8bytes
cout<< sizeof(b)<<endl; // 12bytes
cout<< sizeof(c)<<endl; // 8bytes
很奇怪嗎?定義的三個結構體,只是換了一下結構體中定義的成員的先後順序,怎麼最終得到的結構體所佔用的記憶體大小卻不一樣呢?很詭異嗎?好了,這就是我這裡要總結的記憶體對齊概念了。
記憶體對齊
記憶體對齊的問題主要存在於理解struct和union等複合結構在記憶體中的分布。許多實際的電腦系統對基本類型資料在記憶體中存放的位置有限制,它們會要求這些資料的首地址的值是某個數k(通常它為4或8)的倍數,這就是所謂的記憶體對齊。這個值k在不同的CPU平台下,不同的編譯器下表現也有所不同,現在我們涉及的主流的編譯器是Microsoft的編譯器和GCC。
對於我們這種做上層應用的程式員來說,真的是很少考慮記憶體對齊這個問題的,記憶體對齊對於上層程式員來說,是“透明的”。記憶體對齊,可以說是編譯器做的工作,編譯器為程式中的每個資料區塊安排在適當的記憶體位置上。很多時候,我們要寫出效率更高的代碼,此時我們就需要去瞭解這種記憶體對齊的概念,以及編譯器在後面到底偷偷摸摸幹了點什麼。特別是對於C和C++程式員來說,理解和掌握記憶體對齊更是重要的。
為什麼要有記憶體對齊呢?該佔用多大的記憶體,那就開闢對應大小的記憶體就好了,好比上面的結構體,兩個char類型和一個int類型,大小應該是6bytes才對啊,怎麼又是8bytes,又是12bytes的啊?對於記憶體對齊,主要是為了提高程式的效能,資料結構,特別是棧,應該儘可能地在常態範圍上對齊。原因在於,為了訪問未對其的記憶體,處理器需要做兩次記憶體訪問;然而,對齊的記憶體訪問僅僅需要一次記憶體訪問。
在電腦中,字、雙字和四字在常態範圍上不需要在記憶體中對齊(對字、雙字和四字來說,常態範圍分別是偶數地址,可以被4整除的地址和可以被8整除的地址)。如果一個字或雙字運算元跨越了4位元組邊界,或者一個四字運算元跨越了8位元組邊界,就被認為是未對齊的,從而需要兩次匯流排周期來訪問記憶體。一個字起始地址是奇數,但卻沒有跨越字邊界,就被認為是對齊的,能夠在一個匯流排周期中被訪問。綜上所述,記憶體對齊可以用一句話來概括——資料項目只能儲存在地址是資料項目大小的整數倍的記憶體位置上。
我們再來看看一個簡答的例子:
複製代碼 代碼如下:
#include <stdio.h>
struct Test
{
char a;
int b;
int c;
char d;
};
int main()
{
struct Test structTest;
printf("&a=%p\n", &structTest.a);
printf("&b=%p\n", &structTest.b);
printf("&c=%p\n", &structTest.c);
printf("&d=%p\n", &structTest.d);
printf("sizeof(Test)=%d\n", sizeof(structTest));
return 0;
}
輸出結果如下:
複製代碼 代碼如下:
&a=00C7FA44
&b=00C7FA48
&c=00C7FA4C
&d=00C7FA50
sizeof(Test)=16
結構體Test的成員變數b佔用位元組數為4bytes,所以只能儲存在4的整數倍的位置上,由於a只佔用1一個位元組,而a的地址00C7FA44和b的地址00C7FA48之間相差4bytes,這就說明,a其實也佔用了4個位元組,這樣才能保證b的起始地址是4的整數倍。這就是記憶體對齊。如果沒有記憶體對齊,我們再拿上面的代碼作為例子,則可能輸出結果如下:
複製代碼 代碼如下:
&a=ffbff5e8
&b=ffbff5e9
&c=ffbff5ed
&d=ffbff5f1
sizeof(Test)=10
可以看到,a佔用了一個位元組,緊接著a之後就是b;之前也說了,記憶體對齊是作業系統為了快速存取記憶體而採用的一種策略,簡單來說,就是為了防止變數的二次訪問。作業系統在訪問記憶體時,每次讀取一定的長度(這個長度就是作業系統的預設對齊係數,或者是預設對齊係數的整數倍)。沒有了記憶體對齊,當我們讀取變數c時,第一次讀取0xffbff5e8~0xffbff5ef的記憶體,第二次讀取0xffbff5f0~0xffbff5f8的記憶體,由於變數c所佔用的記憶體跨越了兩片地址地區,為了正確得到變數c的值,就需要讀取兩次,將兩次記憶體合并進行整合,這樣就降低了記憶體的訪問效率。
我在這裡說了這麼多,也挺繞口,這就是記憶體對齊的規則。在C++中,每個特定平台上的編譯器都有自己的記憶體對齊規則,下面我們就記憶體對齊的規則進行總結。
記憶體對齊規則
每個特定平台上的編譯器都有自己的預設“對齊係數”。我們可以通過先行編譯命令#pragma pack(k),k=1,2,4,8,16來改變這個係數,其中k就是需要指定的“對齊係數”;也可以使用#pragma pack()取消自訂位元組對齊。具體的對齊規則如下:
規則1:struct或者union的資料成員對齊規則:第一個資料成員放在offset為0的地方,對齊按照#pragma pack指定的數值和自身佔用位元組數中,二者比較小的那個進行對齊;比如;
複製代碼 代碼如下:
#pragma pack(4) // 指定對齊係數為4,當佔用位元組數大於等於4時,就按照4進行對齊
struct Test
{
char x1;
short x2;
float x3;
char x4;
};
x1佔用位元組數為1,1 < 4,按照對齊係數1進行對齊,所以x1放置在offset為0的位置;
x2佔用位元組數為2,2 < 4,按照對齊係數2進行對齊,所以x2放置在offset為2,3的位置;
x3佔用位元組數為4,4 = 4,按照對齊係數4進行對齊,所以x3放置在offset為4,5,6,7的位置;
x4佔用位元組數為1,1 < 4,按照對齊係數1進行對齊,所以x4放置在offset為8的位置;
現在已經佔了9bytes的記憶體空間了,但是實際在visual studio 2012中實測為12bytes,為什麼呢?看下一條規則。
規則2:struct或者union的整體對齊規則:在資料成員完成各自對齊以後,struct或者union本身也要進行對齊,對齊將按照#pragma pack指定的數值和struct或者union中最大資料成員長度中比較小的那個進行;
繼續使用規則1種的例子進行解釋,按照規則1的理解,struct Test已經佔用了9bytes,實際為什麼是12bytes呢?根據規則2,在所有成員對齊完成以後,struct或者union自身也要進行對齊;我們設定的對齊係數為4,而struct Test中佔用位元組數最大的是float類型的x3,由於x3佔用位元組數小於或等於設定的對齊係數4,所以struct或者union整體需要按照4bytes進行對齊,也就是說,struct或者union佔用的位元組數必須能夠被4整除,好了。struct Test已經佔用了9bytes了,10bytes不能被4整除,11bytes也不能,12bytes正好;所以,struct Test最終佔用的位元組數為12bytes。
上述兩條規則就是記憶體對齊的基本規則,先局部對齊,後整體對齊。
執行個體分析
總結了那麼多的規則,不來點實際的code,總覺的少點什麼,好吧。以下就按照上述總結的記憶體對齊規則,來進行一些實際的程式碼分析(註:測試環境Windows 8.1 + Visual Studio 2012 update 3)。
測試代碼如下,先確認測試環境:
複製代碼 代碼如下:
#include <iostream>
using namespace std;
struct Test
{
char x1;
double x2;
short x3;
float x4;
char x5;
};
int main()
{
cout<<"sizeof(char)"<<sizeof(char)<<endl; // 1byte
cout<<"sizeof(short)"<<sizeof(short)<<endl; // 2bytes
cout<<"sizeof(int)"<<sizeof(int)<<endl; // 4bytes
cout<<"sizeof(double)"<<sizeof(double)<<endl; // 8bytes
return 0;
}
我分別設定#pragma pack(k),k=1,2,4,8,16進行測試。
複製代碼 代碼如下:
#pragma pack(1) // 設定對齊係數為1
struct Test
{
char x1;
double x2;
short x3;
float x4;
char x5;
};
首先使用規則1,對成員變數進行對齊:
x1 <= 1,按照1進行對齊,x1佔用0;
x2 > 1,按照1進行對齊,x2佔用1,2,3,4,5,6,7,8;
x3 > 1,按照1進行對齊,x3佔用9,10;
x4 > 1,按照1進行對齊,x4佔用11,12,13,14;
x5 > 1,按照1進行對齊,x5佔用15;
最後使用規則2,對struct整體進行對齊:
x2佔用記憶體最大,為8bytes,8bytes > 1byte,所以整體按照1進行對齊;16%1=0。
所以,在#pragma pack(1) 的情況下,struct Test佔用記憶體為16bytes;記憶體佔用如下圖所示:
複製代碼 代碼如下:
#pragma pack(2) // 設定對齊係數為2
struct Test
{
char x1;
double x2;
short x3;
float x4;
char x5;
};
首先使用規則1,對成員變數進行對齊:
x1 <= 2,按照1進行對齊,x1佔用0;
x2 > 2,按照2進行對齊,x2佔用2,3,4,5,6,7,8,9;
x3 >= 2,按照2進行對齊,x3佔用10,11;
x4 > 2,按照2進行對齊,x4佔用12,13,14,15;
x5 < 2,按照1進行對齊,x5佔用16;
最後使用規則2,對struct整體進行對齊:
x2佔用記憶體最大,為8bytes,8bytes > 2byte,所以整體按照2進行對齊;17%2!=0
所以,在#pragma pack(2) 的情況下,struct Test佔用記憶體為18bytes;記憶體佔用如下圖所示:
複製代碼 代碼如下:
#pragma pack(4) // 設定對齊係數為4
struct Test
{
char x1;
double x2;
short x3;
float x4;
char x5;
};
首先使用規則1,對成員變數進行對齊:
x1 <= 4,按照1進行對齊,x1佔用0;
x2 > 4,按照4進行對齊,x2佔用4,5,6,7,8,9,10,11;
x3 < 4,按照2進行對齊,x3佔用12,13;
x4 >= 4,按照4進行對齊,x4佔用16,17,18,19;
x5 < 4,按照1進行對齊,x5佔用20;
最後使用規則2,對struct整體進行對齊:
x2佔用記憶體最大,為8bytes,8bytes > 4byte,所以整體按照4進行對齊;21%4!=0
所以,在#pragma pack(4) 的情況下,struct Test佔用記憶體為24bytes;記憶體佔用如下圖所示:
複製代碼 代碼如下:
#pragma pack(8) // 設定對齊係數為8
struct Test
{
char x1;
double x2;
short x3;
float x4;
char x5;
};
首先使用規則1,對成員變數進行對齊:
x1 <= 8,按照1進行對齊,x1佔用0;
x2 >= 8,按照8進行對齊,x2佔用8,9,10,11,12,13,14,15;
x3 < 8,按照2進行對齊,x3佔用16,17;
x4 <= 8,按照4進行對齊,x4佔用20,21,22,23;
x5 < 8,按照1進行對齊,x5佔用24;
最後使用規則2,對struct整體進行對齊:
x2佔用記憶體最大,為8bytes,8bytes >= 8byte,所以整體按照8進行對齊;25%8!=0
所以,在#pragma pack(8) 的情況下,struct Test佔用記憶體為32bytes;記憶體佔用如下圖所示:
複製代碼 代碼如下:
#pragma pack(16) // 設定對齊係數為16
struct Test
{
char x1;
double x2;
short x3;
float x4;
char x5;
};
首先使用規則1,對成員變數進行對齊:
x1 < 16,按照1進行對齊,x1佔用0;
x2 < 16,按照8進行對齊,x2佔用8,9,10,11,12,13,14,15;
x3 < 16,按照2進行對齊,x3佔用16,17;
x4 < 16,按照4進行對齊,x4佔用20,21,22,23;
x5 < 16,按照1進行對齊,x5佔用24;
最後使用規則2,對struct整體進行對齊:
x2佔用記憶體最大,為8bytes,16bytes >= 8byte,所以整體按照8進行對齊;25%8!=0
所以,在#pragma pack(16) 的情況下,struct Test佔用記憶體為32bytes;記憶體佔用如下圖所示:
總結
經過上面的執行個體分析,我對記憶體對齊有了全面的認識和瞭解。現在再回過來看看文章開頭的那段代碼,問題就迎刃而解了,同時經過這段代碼,讓我們認識到定義struct或者union時,也是有講解的。在以後的編碼生涯時,是不是又要多考慮一些呢?糾結~