標籤:style blog http color 使用 io strong ar 檔案
編譯器為什麼要替我們記憶體對齊? 學了電腦群組成原理,瞭解了記憶體的基本單元是一個位元組,記憶體可以隨機定址,於是乎我天真的認為記憶體就是一個位元組型的容器,基本單位是單個位元組。
Figure 1. 我眼中的記憶體空間布局 悲劇的是,記憶體讀寫的真正訪問者cpu不是這麼想的。cpu是根據記憶體訪問粒度(memory access granularity,下文簡寫成MAG)來讀取記憶體,MAG就是cpu一次記憶體訪問操作的資料量,具體數值依賴於特定的平台,一般是2byte、4byte、8byte。
Figure 2. cpu眼中的記憶體空間布局 因此,程式員和cpu看待記憶體空間布局是存在差異的。唉,既不能過多的苛刻程式員,又要讓cpu舒服,怎麼辦呢?只好讓編譯器來對我們的代碼進行隱式的記憶體對齊(當然它能做的只是幫程式中的資料進行記憶體對齊,至於直接用指標去訪問記憶體,它是不會管的)。
向開發編譯器的大大們致敬! 記憶體對齊的好處 下面用一個小執行個體來剖析記憶體對齊的好處:很簡單,在32位的機器下,分別訪問一個記憶體對齊的地址空間(從地址0開始)和一個沒有對齊的地址空間(從地址1開始),讀取四個位元組到cpu的寄存器中,比較兩者的讀取過程。
Case1:記憶體訪問粒度為1個位元組(cpu眼中的記憶體模型等價於程式員眼中的記憶體模型): Figure 3. MAG=1 Result:讀取4個位元組,兩者都需要進行4次記憶體訪問操作。打平,在MAG=1的情況下不需要考慮記憶體對齊。
Case2:記憶體訪問粒度為2個位元組: Figure 4. MAG=2 Result:讀取4個位元組,左邊的(記憶體對齊地址)只需要進行2次記憶體訪問操作,右邊的需要進行3次記憶體訪問操作+附加操作(見下文)。記憶體對齊地址取勝!
Case3:記憶體訪問粒度為4個位元組: Figure 5. MAG=4
Result:讀取4個位元組,左邊的只需要進行1次記憶體訪問操作,右邊的需要進行2次記憶體訪問操作+附加操作。記憶體對齊地址再次取勝!
Conclusion:記憶體對齊地址vs沒有記憶體對齊的地址,在三種不同的記憶體訪問粒度下,取得了2勝一平的完勝戰績。對於32位的機器,實際的記憶體訪問粒度是4個位元組,原因如下:
- 每一次記憶體訪問操作需要一個常量開銷;
- 在資料量一定的情況下,減少記憶體訪問操作,能提高程式運行效能;
- 增大記憶體訪問粒度(當然不超過資料匯流排的頻寬),能減少記憶體訪問操作(從上面的執行個體就能夠看出來);
一句話,記憶體對齊確實可以提高程式效能。 cpu如何處理沒有記憶體對齊的資料訪問? 繼續分析上面那個執行個體,在記憶體訪問粒度為2、從地址1開始讀取四個位元組的cpu處理過程(硬體方式):
- 讀取資料所在的第一塊記憶體空間(0-1),移除多餘位元組(0);
- 讀取資料所在的第二塊記憶體空間(2-3);
- 讀取資料所在的第三塊記憶體空間(4-5),移除多餘位元組(5);
- 把三塊資料拼接起來(1-4),放入寄存器中。
訪問一塊相同大小的資料,記憶體對齊的優勢是多麼的巨大!如果cpu能這麼來處理,也只不過是影響了我們程式的運行效能,至少還是能啟動並執行!悲劇的是,以前的cpu並沒有這麼“勤快”,遇到沒有記憶體對齊的資料訪問,它會直接拋出一個異常:作業系統可能會響應這個異常,用軟體的方式來處理,效能只會更差;或者程式直接崩潰掉。
一句話,記憶體對齊的代碼確實具有更高的可移植性! 結構體在記憶體中的對齊個人總結:結構體預設的對齊模數不是sizeof(結構體),而是結構體成員中最大的對齊模數。例如
struct A{ int a;//預設對齊模數k(int)=sizeof(int)=4 char b;//預設對齊模數k(char)=sizeof(char)=1 struct B d;//預設對齊模數k(struct B)為B成員最大的對齊模數即k(double)=8};//A預設對齊模數k(struct A)為A成員最大的對齊模數k(struct B)=8struct B{ int a;//預設對齊模數k(int)=sizeof(int)=4 char b;//預設對齊模數k(char)=sizeof(char)=1 double c;//預設對齊模數k(double)=sizeof(double)=8 short ;//預設對齊模數k(short)=sizeof(short)=2};//B預設對齊模數k(struct B)為B成員最大的對齊模數即k(double)=8
由上面的兩個結構體可以看出 :
基礎資料型別 (Elementary Data Type)的預設對齊模數k=sizeof(type);
複雜資料類型如結構體,其預設對齊模數k=max{各個成員的預設對齊模數};
儲存規則:
1.當系統指定了對齊模數k時,不同類型的資料按照k(m)=min{預設的對齊模數k,系統指定的k}模數來儲存資料;
2.該類型的資料起始地址add%k(m)=0;為什麼要這樣滿足這個條件呢?
其原因是各個硬體平台對儲存空間的處理上有很大的不同。一些平台對某些特定類型的資料只能從某些特定地址開始存取。比如有些平台每次讀都是從偶地址開始,如果一個int型(假設為32位系統)如果存放在偶地址開始的地方,那 麼一個讀周期就可以讀出這32bit,而如果存放在奇地址開始的地方,就需要2個讀周期,並對兩次讀出的結果的高低位元組進行拼湊才能得到該32bit數 據。顯然在讀取效率上下降很多。
3.該類型的資料大小size%k(m)=0,不足的要補齊(結構體中經常需要補齊);為什麼要這樣滿足這個條件呢?
因為考慮到該類型的數組,按照數組的定義,數組中所有元素都是緊挨著的。
結構體記憶體配置問題(轉)1)sizeof也可以對一個函數調用求值,其結果是函數傳回型別的大小,函數並不會被調用。
(2)終於搞懂struct結構體記憶體配置問題了,結構體中各個成員位元組對齊遵循以下幾個原則:
1.結構體每個成員相對於結構體首地址的位移量(offset)都是(這個)成員大小的整數倍,如有需要編譯器會在成員之間加上填充位元組(internal adding);
例如有以下一個結構體
struct ex {
int i;
char t;
int n;
}
第1個成員位移量為0,是int型成員大小4(假設這太機器的整型長度佔4個位元組)的整數倍。
第2個成員 t 為char型,他的大小為1,首先假設在成員i和t之間沒有填充位元組,由於i是整型,佔4個位元組那麼在沒有填充之前,第2個成員t相對於結構體的位移量為4,他是t成員大小1的4倍,符合此條件,所以系統在給結構體第2個成員分配記憶體時,不會在i和t之間填充位元組以到達對齊的目的。
當分配結構體第3個成員n時,首先發現是一個整型資料,大小為4,沒有填充之前,n相對於結構體首地址位移量為:前面2個成員+填充位元組=5,所以當系統發現5不是4(成員大小)的整數倍時,會在成員t之後(或者說n之前)填充3個位元組,以使n的位移量到達8而成為4的整數倍。這樣這個結構體佔用記憶體情況暫時為4+1+3+4。
2.結構體的總大小為結構體最寬基本類型成員大小的整數倍,如有需要編譯器會在最末一個成員之後加上填充位元組(trailing padding)。
上面的結構體記憶體配置以後還要看是否滿足此條件,假設在最末一個成員之後不需填充位元組數,那麼這個結構體的大小為12。而ex結構體中最寬基本類型成員為int,大小為4,12為4的整數倍,所以無須再在最末一個成員之後加上填充位元組了。所以sizeof(ex)=12;
如果一個結構體如下所示struct ex1{
int i;
char t;
int n;
char add;
}
那麼sizeof(ex1) =16;原因就是在最後一個成員之後填充了3個位元組。
3.還有一個額外的條件:結構體變數的首地址能夠被其最寬基本類型成員的大小所整除;
4.對於結構體成員屬性中包含結構體變數的複合型結構體再確定最寬基本類型成員時,應當包括複合類型成員的子成員。但在確定複合類型成員的位移位置時則是將複合類型作為整體看待。
5總結出一個公式:結構體的大小等於最後一個成員的位移量加上其大小再加上末尾的填充位元組數目,即:
sizeof( struct ) = offsetof( last item ) + sizeof( last item ) + sizeof( trailing padding )
C/C++ 中已經提供了宏 offsetof 導出成員的位移量。注意包含標頭檔:C 是 <stddef.h>,C++ 是 <cstddef>。 ------------------------------------------------------------------------------------------------------------------------------------
由於程式運行時佔用的記憶體過大,所以想辦法給程式瘦身。
在調試中發現結構體佔用的size竟然和預想的不一樣,原來……
看看下面講的吧,肯定會不枉此看哦!
1,比如:
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
因為:“成員對齊有一個重要的條件,即每個成員按自己的方式對齊.其對齊的規則是,每個成員按其類型的對齊參數(通常是這個類型的大小)和指定對齊參數(這裡預設是8位元組)中較小的一個對齊.並且結構的長度必須為所用過的所有對齊參數的整數倍,不夠就補空位元組.”(引用)
結構體A中有3個short類型變數,各自以2位元組對齊,結構體對齊參數按預設的8位元組對齊,則a1,a2,a3都取2位元組對齊,則sizeof(A)為6,其也是2的整數倍;
B中a1為4位元組對齊,a2為2位元組對齊,結構體預設對齊參數為8,則a1取4位元組對齊,a2取2位元組對齊,結構體大小6位元組,6不為4的整數倍,補空位元組,增到8時,符合所有條件,則sizeof(B)為8;
可以設定成對齊的
#pragma pack(1)
#pragma pack(push)
#pragma pack(1)
struct{
short a1;
short a2;
short a3;
}A;
struct{
long a1;
short a2;
}B;
#pragma pack(pop)
結果為sizeof( A)=6,sizeof( B)=6
************************
2,又如:
#pragma pack(8)
struct S1{
char a;
long b;
};
struct S2 {
char c;
struct S1 d;
long long e;
};
#pragma pack()
sizeof(S2)結果為24.
成員對齊有一個重要的條件,即每個成員分別對齊.即每個成員按自己的方式對齊.
也就是說上面雖然指定了按8位元組對齊,但並不是所有的成員都是以8位元組對齊.其對齊的規則是,每個成員按其類型的對齊參數(通常是這個類型的大小)和指定對齊參數(這裡是8位元組)中較小的一個對齊.並且結構的長度必須為所用過的所有對齊參數的整數倍,不夠就補空位元組.
S1中,成員a是1位元組預設按1位元組對齊,指定對齊參數為8,這兩個值中取1,a按1位元組對齊;成員b是4個位元組,預設是按4位元組對齊,這時就按4位元組對齊,所以sizeof(S1)應該為8;
S2 中,c和S1中的a一樣,按1位元組對齊,而d 是個結構,它是8個位元組,它按什麼對齊呢?對於結構來說,它的預設對齊就是它的所有成員使用的對齊參數中最大的一個,S1的就是4.所以,成員d就是按4位元組對齊.成員e是8個位元組,它是預設按8位元組對齊,和指定的一樣,所以它對到8位元組的邊界上,這時, 已經使用了12個位元組了,所以又添加了4個位元組的空,從第16個位元組開始放置成員e.這時,長度為24,已經可以被8(成員e按8位元組對齊)整除.這樣, 一共使用了24個位元組.
a b
S1的記憶體布局:1***, 1111,
c S1.a S1.b e
S2的記憶體布局:1***, 1***, 1111, ****11111111
這裡有三點很重要:
1.每個成員分別按自己的方式對齊,並能最小化長度
2.複雜類型(如結構)的預設對齊是它最長的成員的對齊,這樣在成員是複雜類型時,可以最小化長度
3.對齊後的長度必須是成員中最大的對齊參數的整數倍,這樣在處理數組時可以保證每一項都邊界對齊
補充一下,對於數組,比如:
char a[3];這種,它的對齊和分別寫3個char是一樣的.也就是說它還是按1個位元組對齊.
如果寫: typedef char Array3[3];
Array3這種類型的對齊還是按1個位元組對齊,而不是按它的長度.
不論類型是什麼,對齊的邊界一定是1,2,4,8,16,32,64....中的一個.
位元組對齊詳解
一.為什麼要對齊?
現代電腦中記憶體空間都是按照byte劃分的,從理論上講似乎對任何類型的變數的訪問可以從任何地址開始,但實際情況是在訪問特定類型變數的時候經常在特 定的記憶體位址訪問,這就需要各種類型資料按照一定的規則在空間上排列,而不是順序的一個接一個的排放,這就是對齊。
對齊的作用和原因:各個硬體平台對儲存空間的處理上有很大的不同。一些平台對某些特定類型的資料只能從某些特定地址開始存取。比如有些架構的CPU在訪問 一個沒有進行對齊的變數的時候會發生錯誤,那麼在這種架構下編程必須保證位元組對齊.其他平台可能沒有這種情況,但是最常見的是如果不按照適合其平台要求對 資料存放進行對齊,會在存取效率上帶來損失。比如有些平台每次讀都是從偶地址開始,如果一個int型(假設為32位系統)如果存放在偶地址開始的地方,那 麼一個讀周期就可以讀出這32bit,而如果存放在奇地址開始的地方,就需要2個讀周期,並對兩次讀出的結果的高低位元組進行拼湊才能得到該32bit數 據。顯然在讀取效率上下降很多。
二.位元組對齊對程式的影響:
先讓我們看幾個例子吧(32bit,x86環境,gcc編譯器):
設結構體如下定義:
struct A
{
int a;
char b;
short c;
};
struct B
{
char b;
int a;
short c;
};
現在已知32位機器上各種資料類型的長度如下:
char:1(有符號無符號同)
short:2(有符號無符號同)
int:4(有符號無符號同)
long:4(有符號無符號同)
float:4 double:8
那麼上面兩個結構大小如何呢?
結果是:
sizeof(strcut A)值為8
sizeof(struct B)的值卻是12
結構體A中包含了4位元組長度的int一個,1位元組長度的char一個和2位元組長度的short型資料一個,B也一樣;按理說A,B大小應該都是7位元組。
之所以出現上面的結果是因為編譯器要對資料成員在空間上進行對齊。上面是按照編譯器的預設設定進行對齊的結果,那麼我們是不是可以改變編譯器的這種預設對齊設定呢,當然可以.例如:
#pragma pack (2) /*指定按2位元組對齊*/
struct C
{
char b;
int a;
short c;
};
#pragma pack () /*取消指定對齊,恢複預設對齊*/
sizeof(struct C)值是8。
修改對齊值為1:
#pragma pack (1) /*指定按1位元組對齊*/
struct D
{
char b;
int a;
short c;
};
#pragma pack () /*取消指定對齊,恢複預設對齊*/
sizeof(struct D)值為7。
後面我們再講解#pragma pack()的作用.
三.編譯器是按照什麼樣的原則進行對齊的?
先讓我們看四個重要的基本概念:
1.資料類型自身的對齊值:
對於char型資料,其自身對齊值為1,對於short型為2,對於int,float,double類型,其自身對齊值為4,單位位元組。
2.結構體或者類的自身對齊值:其成員中自身對齊值最大的那個值。
3.指定對齊值:#pragma pack (value)時的指定對齊值value。
4.資料成員、結構體和類的有效對齊值:自身對齊值和指定對齊值中小的那個值。
有 了這些值,我們就可以很方便的來討論具體資料結構的成員和其自身的對齊。有效對齊值N是最終用來決定資料存放地址方式的值,最重要。有效對齊N,就是 表示“對齊在N上”,也就是說該資料的"存放起始地址%N=0".而資料結構中的資料變數都是按定義的先後順序來排放的。第一個資料變數的起始地址就是數 據結構的起始地址。結構體的成員變數要對齊排放,結構體本身也要根據自身的有效對齊值圓整(就是結構體成員變數佔用總長度需要是對結構體有效對齊值的整數 倍,結合下面例子理解)。這樣就不能理解上面的幾個例子的值了。
例子分析:
分析例子B;
struct B
{
char b;
int a;
short c;
};
假 設B從地址空間0x0000開始排放。該例子中沒有定義指定對齊值,在筆者環境下,該值預設為4。第一個成員變數b的自身對齊值是1,比指定或者預設指定 對齊值4小,所以其有效對齊值為1,所以其存放地址0x0000符合0x0000%1=0.第二個成員變數a,其自身對齊值為4,所以有效對齊值也為4, 所以只能存放在起始地址為0x0004到0x0007這四個連續的位元組空間中,複核0x0004%4=0,且緊靠第一個變數。第三個變數c,自身對齊值為 2,所以有效對齊值也是2,可以存放在0x0008到0x0009這兩個位元組空間中,符合0x0008%2=0。所以從0x0000到0x0009存放的 都是B內容。再看資料結構B的自身對齊值為其變數中最大對齊值(這裡是b)所以就是4,所以結構體的有效對齊值也是4。根據結構體圓整的要求, 0x0009到0x0000=10位元組,(10+2)%4=0。所以0x0000A到0x000B也為結構體B所佔用。故B從0x0000到0x000B 共有12個位元組,sizeof(struct B)=12;其實如果就這一個就來說它已將滿足位元組對齊了, 因為它的起始地址是0,因此肯定是對齊的,之所以在後面補充2個位元組,是因為編譯器為了實現結構數組的存取效率,試想如果我們定義了一個結構B的數組,那 麼第一個結構起始地址是0沒有問題,但是第二個結構呢?按照數組的定義,數組中所有元素都是緊挨著的,如果我們不把結構的大小補充為4的整數倍,那麼下一 個結構的起始地址將是0x0000A,這顯然不能滿足結構的地址對齊了,因此我們要把結構補充成有效對齊大小的整數倍.其實諸如:對於char型資料,其 自身對齊值為1,對於short型為2,對於int,float,double類型,其自身對齊值為4,這些已有類型的自身對齊值也是基於數組考慮的,只 是因為這些類型的長度已知了,所以他們的自身對齊值也就已知了.
同理,分析上面例子C:
#pragma pack (2) /*指定按2位元組對齊*/
struct C
{
char b;
int a;
short c;
};
#pragma pack () /*取消指定對齊,恢複預設對齊*/
第 一個變數b的自身對齊值為1,指定對齊值為2,所以,其有效對齊值為1,假設C從0x0000開始,那麼b存放在0x0000,符合0x0000%1= 0;第二個變數,自身對齊值為4,指定對齊值為2,所以有效對齊值為2,所以順序存放在0x0002、0x0003、0x0004、0x0005四個連續 位元組中,符合0x0002%2=0。第三個變數c的自身對齊值為2,所以有效對齊值為2,順序存放
在0x0006、0x0007中,符合 0x0006%2=0。所以從0x0000到0x00007共八位元組存放的是C的變數。又C的自身對齊值為4,所以C的有效對齊值為2。又8%2=0,C 只佔用0x0000到0x0007的八個位元組。所以sizeof(struct C)=8.
四.如何修改編譯器的預設對齊值?
1.在VC IDE中,可以這樣修改:[Project]|[Settings],c/c++選項卡Category的Code Generation選項的Struct Member Alignment中修改,預設是8位元組。
2.在編碼時,可以這樣動態修改:#pragma pack .注意:是pragma而不是progma.
五.針對位元組對齊,我們在編程中如何考慮?
如果在編程的時候要考慮節約空間的話,那麼我們只需要假定結構的首地址是0,然後各個變數按照上面的原則進行排列即可,基本的原則就是把結構中的變數按照 類型大小從小到大聲明,盡量減少中間的填補空間.還有一種就是為了以空間換取時間的效率,我們顯示的進行填補空間進行對齊,比如:有一種使用空間換時間做 法是顯式的插入reserved成員:
struct A{
char a;
char reserved[3];//使用空間換時間
int b;
}
reserved成員對我們的程式沒有什麼意義,它只是起到填補空間以達到位元組對齊的目的,當然即使不加這個成員通常編譯器也會給我們自動填補對齊,我們自己加上它只是起到顯式的提醒作用.
Over!更多內容,參照此文:https://www.ibm.com/developerworks/library/pa-dalign/記憶體對齊的具體規則,參照此篇部落格:http://blog.csdn.net/liu1064782986/article/details/7600979
記憶體對齊之深度探索