1. 程式人生 > >記憶體分配與記憶體對齊全面探討

記憶體分配與記憶體對齊全面探討

引言

作業系統的記憶體分配問題與記憶體對齊問題對於低層程式設計來說是非常重要的,對記憶體分配的理解直接影響到程式碼質量、正確率、效率以及程式設計師對記憶體使用情況、溢位、洩露等的判斷力。而記憶體對齊是常常被忽略的問題,理解記憶體對齊原理及方法則有助於幫助程式設計師判斷訪問非法記憶體。

程式的記憶體分配問題

一般C/C++程式佔用的記憶體主要分為5種

1、棧區(stack):類似於堆疊,由程式自動建立、自動釋放。函式引數、區域性變數以及返回點等資訊都存於其中。
2、堆區(heap): 使用自由,不需預先確定大小。多數情況下需要由程式設計師手動申請、釋放。如不釋放,程式結束後由作業系統垃圾回收機制收回。
3、全域性區/靜態區(static):全域性變數和靜態變數的儲存是區域。程式結束後由系統釋放。
4、文字常量區:常量字串就是放在這裡的。 程式結束後由系統釋放。
5、程式程式碼區:既可執行程式碼。

例:

#include <stdio.h>
int quanju;/*全域性變數,全域性區/靜態區(static)*/
void fun(int f_jubu); /*程式程式碼區*/
int main(void)/**/
{
       int m_jubu;/*棧區(stack)*/
       static int m_jingtai;/*靜態變數,全域性區/靜態區(static)*/
       char *m_zifum,*m_zifuc = "hello";/*指標本身位於棧。指向字串"hello",位於文字常量區*/
       void (*pfun)(int); /*棧區(stack)*/
pfun=&fun; m_zifum = (char *)malloc(sizeof(char)*10);/*指標內容指向分配空間,位於堆區(heap)*/ pfun(1); printf("&quanju : %x/n",&quanju); printf("&m_jubu : %x/n",&m_jubu); printf("&m_jingtai: %x/n",&m_jingtai); printf("m_zifuc : %x/n",m_zifuc); printf
("&m_zifuc : %x/n",&m_zifuc); printf("m_zifum : %x/n",m_zifum); printf("&m_zifum : %x/n",&m_zifum); printf("pfun : %x/n",pfun); printf("&pfun : %x/n",&pfun); getch(); return 0; } void fun(int f_jubu) { static int f_jingtai; printf("&f_jingtai: %x/n",&f_jingtai); printf("&f_jubu : %x/n",&f_jubu);/*棧區(stack),但是與主函式中m_jubu位於不同的棧*/ }

輸出結果:

&f_jingtai: 404020
&f_jubu   : 22ff40
&quanju   : 404070
&m_jubu   : 22ff74
&m_jingtai: 404010
m_zifuc   : 403000
&m_zifuc  : 22ff6c
m_zifum   : 3d24e0
&m_zifum  : 22ff70
pfun      : 4013af
&pfun     : 22ff68

分析:
堆區:
m_zifum : 3d24e0
程式碼區:
pfun : 4013af
局區/靜態區(static):
m_zifuc : 403000
&m_jingtai: 404010
&f_jingtai: 404020
&quanju : 404070
棧區:
&f_jubu : 22ff40 fun函式棧區
&pfun : 22ff68 主函式棧區
&m_zifuc : 22ff6c
&m_zifum : 22ff70
&m_jubu : 22ff74

堆和棧

申請方式

stack:
由系統自動分配。 例如,宣告在函式中一個區域性變數 int b; 系統自動在棧中為b開闢空間
heap:
需要程式設計師手動申請,並指明大小,在c中,有malloc函式完成
如p1 = (char *)malloc(10);
在C++中用new運算子
如p2 = (char *)malloc(10);
但是注意p1、p2本身是在棧中的。

申請後系統的響應

棧:只要棧的剩餘空間大於所申請空間,系統將為程式提供記憶體,否則將報異常提示棧溢位。
堆:大多數作業系統有一個記錄空閒記憶體地址的連結串列,當系統收到程式的申請時,會遍歷該連結串列,尋找第一個空間大於所申請空間的堆結點,然後將該結點從空閒結點連結串列中刪除,並將該結點的空間分配給程式,另外,對於大多數系統,會在這塊記憶體空間中的首地址處記錄本次分配的大小,這樣,程式碼中的free函式才能正確的釋放本記憶體空間。另外,由於找到的堆結點的大小不一定正好等於申請的大小,系統會自動的將多餘的那部分重新放入空閒連結串列中。

申請大小的限制

棧:在Windows下,棧是向低地址擴充套件的資料結構,是一塊連續的記憶體的區域。這句話的意思是棧頂的地址和棧的最大容量是系統預先規定好的,在WINDOWS下,棧的大小是2M(也有的說是1M,總之是一個編譯時就確定的常數),如果申請的空間超過棧的剩餘空間時,將提示overflow。因此,能從棧獲得的空間較小。
堆:堆是向高地址擴充套件的資料結構,是不連續的記憶體區域。這是由於系統是用連結串列來儲存的空閒記憶體地址的,自然是不連續的,而連結串列的遍歷方向是由低地址向高地址。堆的大小受限於計算機系統中有效的虛擬記憶體。由此可見,堆獲得的空間比較靈活,也比較大。

申請效率的比較:

棧由系統自動分配,速度較快。但程式設計師是無法控制的。
堆是由程式設計師手動分配的記憶體,一般速度比較慢,而且容易產生記憶體碎片,不過用起來最方便.
另外,在WINDOWS下,最好的方式是用VirtualAlloc分配記憶體,他不是在堆,也不是在棧是直接在程序的地址空間中保留一快記憶體,雖然用起來最不方便。但是速度快,也最靈活。

堆和棧中的儲存內容

棧: 在函式呼叫時,第一個進棧的是函式呼叫語句的下一條可執行語句的地址,然後是函式的各個引數,在大多數的C編譯器中,引數是由右往左入棧的,然後是函式中的區域性變數。注意靜態變數是不入棧的。 當本次函式呼叫結束後,區域性變數先出棧,然後是引數,最後棧頂指標指向最開始存的地址,也就是函式中的下一條指令,程式由該點繼續執行。
堆:一般是在堆的頭部用一個位元組存放堆的大小。堆中的具體內容由程式設計師安排。

存取效率的比較

char s1[] = "aaaaaaaaaaaaaaa";
char *s2 = "bbbbbbbbbbbbbbbbb";

aaaaaaaaaaa是在執行時刻賦值的;
而bbbbbbbbbbb是在編譯時就確定的;
但是,在以後的存取中,在棧上的陣列比指標所指向的字串(例如堆)快。
比如:

#include
void main()
{
char a = 1;
char c[] = "1234567890";
char *p ="1234567890";
a = c[1];
a = p[1];
return;
}
對應的彙編程式碼
a = c[1];
00401067 8A 4D F1 mov cl,byte ptr [ebp-0Fh]
0040106A 88 4D FC mov byte ptr [ebp-4],cl
a = p[1];
0040106D 8B 55 EC mov edx,dword ptr [ebp-14h]
00401070 8A 42 01 mov al,byte ptr [edx+1]
00401073 88 45 FC mov byte ptr [ebp-4],al
第一種在讀取時直接就把字串中的元素讀到暫存器cl中,而第二種則要先把指標值讀到edx中,在根據edx讀取字元,顯然慢了一些。

記憶體對齊問題

記憶體對齊的原因

大部分的參考資料都是如是說的:
1、平臺原因(移植原因):不是所有的硬體平臺都能訪問任意地址上的任意資料的;某些硬體平臺只能在某些地址處取某些特定型別的資料,否則丟擲硬體異常。
2、效能原因:資料結構(尤其是棧)應該儘可能地在自然邊界上對齊。原因在於,為了訪問未對齊的記憶體,處理器需要作兩次記憶體訪問;而對齊的記憶體訪問僅需要一次訪問。

對齊規則

每個特定平臺上的編譯器都有自己的預設“對齊係數”(也叫對齊模數)。程式設計師可以通過預編譯命令#pragma pack(n),n=1,2,4,8,16來改變這一系數,其中的n就是你要指定的“對齊係數”。
規則:
1、資料成員對齊規則:結構(struct)(或聯合(union))的資料成員,第一個資料成員放在offset為0的地方,以後每個資料成員的對齊按照#pragma pack指定的數值和這個資料成員
自身長度中,比較小的那個進行。
2、結構(或聯合)的整體對齊規則:在資料成員完成各自對齊之後,結構(或聯合)本身也要進行對齊,對齊將按照#pragma pack指定的數值和結構(或聯合)最大資料成員長度中,比較小的那個進行。
3、結合1、2可推斷:當#pragma pack的n值等於或超過所有資料成員長度的時候,這個n值的大小將不產生任何效果。

試驗

下面我們通過一系列例子的詳細說明來證明這個規則
編譯器:GCC 3.4.2、VC6.0
平臺:Windows XP

典型的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

試驗過程如下:通過#pragma pack(n)改變“對齊係數”,然後察看sizeof(struct test_t)的值。

1位元組對齊(#pragma pack(1))

輸出結果:sizeof(struct test_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位元組對齊(#pragma pack(2))

輸出結果:sizeof(struct test_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 */

4位元組對齊(#pragma pack(4))

輸出結果:sizeof(struct test_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 */

8位元組對齊(#pragma pack(8))

輸出結果:sizeof(struct test_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 */

16位元組對齊(#pragma pack(16))

輸出結果:sizeof(struct test_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 */
8位元組和16位元組對齊試驗證明了“規則”的第3點:“當#pragma pack的n值等於或超過所有資料成員長度的時候,這個n值的大小將不產生任何效果”。

結束語

記憶體分配與記憶體對齊是個很複雜的東西,不但與具體實現密切相關,而且在不同的作業系統,編譯器或硬體平臺上規則也不盡相同,雖然目前大多數系統/語言都具有自動管理、分配並隱藏低層操作的功能,使得應用程式編寫大為簡單,程式設計師不在需要考慮詳細的記憶體分配問題。但是,在系統或驅動級以至於高實時,高保密性的程式開發過程中,程式記憶體分配問題仍舊是保證整個程式穩定,安全,高效的基礎。

[參考文獻及技術支援]
[1] Brian.W.Kerighan <the C programming language> 2004.1
[2] W.richard stevens <unix環境高階程式設計> 2006.10
[3]csdn開發社群 c/c++版塊 提供技術支援
[4]50M深藍程式設計討論組 提供技術支援

[注1]
什麼是“圓整”?
舉例說明:如上面的8位元組對齊中的“整體對齊”,整體大小=9 按 4 圓整 = 12
圓整的過程:從9開始每次加一,看是否能被4整除,這裡9,10,11均不能被4整除,到12時可以,則圓整結束。