大小端、位段和内存对齐
生活随笔
收集整理的這篇文章主要介紹了
大小端、位段和内存对齐
小編覺得挺不錯的,現在分享給大家,幫大家做個參考.
關于C++中的大小端、位段(惑位域)和內存對齊 來源: ♂戚建建㊣的日志 聽到好幾個朋友說到去一些公司做面試,總是遇到關于大小端、位段(或者叫位域)和內存對齊的考題,然后就不知所措了。雖然我認為很多開發根本就用不到這個,但是我認為很有必要學習理解這些知識點,因為它可以讓你更了解C++的,了解程序在內存的運行情況,也能加深對計算機系統的理解。 聲明:由于本文的代碼會受到計算機環境的影響,故在此說明本篇博文中的程序的運行環境。 1、Microsoft Windows 7 Ultimate Edition Service Pack 1 (64bit 6.1.7601) 2、Microsoft Visual Studio 2010 Version 10.0.40219.1 SP1Rel(Ultimate--ENU)。 3、Microsoft .NET Framework Version 4.0.30319 SP1Rel 4、Microsoft Visual C++ 2010 注:雖然系統是64位的,但是我是使用VC++ 2010默認配置,也即是x86平臺。所以下面所有示例和文字表述都是基于32位編譯平臺。一、大小端 在現代“馮.諾依曼體系結構”計算機中,它的數制都是采用二進制來存儲,并且是以8位,一個字節為單位,產生內存地址系統。數據在內存中有如下三種存在方式: 1、從靜態存儲區分配:此時的內存在程序編譯的時候已經分配好,并且在程序的整個運行期間都存在。全局變量,static變量等在此存儲。 2、在棧區分配:在程序的相關代碼執行時創建,執行結束時被自動釋放。局部變量在此存儲。棧內存分配運算內置于處理器的指令集中,效率 高,但容量有限。 3、在堆區分配:動態分配內存。用new/malloc時開辟,delete/free時釋放。變量的生存期由用戶指定,靈活,但會有內存泄露等問題。 對于像C++中的char這樣的數據類型,它本身就是占用一個字節的大小,不會產生什么問題。但是當數制類型為int,在32bit的系統中,它需要占用4個字節(32bit),這個時候就會產生這4個字節在寄存器中的存放順序的問題。比如int maxHeight = 0x12345678,&maxHeight = 0x0042ffc4。具體的該怎么存放呢?這個時候就需要理解計算機的大小端的原理了。 大端:(Big-Endian)就是把數值的高位字節放在內存的地位地址上,把數值的地位字節放在內存的地位地址上。 小端:(Little-Endian)就是把數字的高位字節放在高位的地址上,地位字節放在地位地址上。 我們常用的x86結構都是小端模式,而大部分DSP,ARM也是小端模式,不過有些ARM是可以選擇大小端模式。所以對于上面的maxHeight是應該以小端模式來存放,具體情況請看下面兩表。 地址 0x0042ffc4 0x0042ffc5 0x0042ffc6 0x0042ffc7 數值 0x78 0x56 0x34 0x12 上圖為小端模式 地址 0x0042ffc4 0x0042ffc5 0x0042ffc6 0x0042ffc7 數值 0x12 0x34 0x56 0x78 上圖為大端模式 通過上面的表格,可以看出來大小端的不同,在這里無法討論那種方式更好,個人覺得似乎大端模式更符合我的習慣。(注:在這里我還要說一句,其實在計算機內存中并不存在所謂的數據類型,比如char,int等的。這個類型在代碼中的作用就是讓編譯器知道每次應該從那個地址起始讀取多少位的數據,賦值給相應的變量。) 二、位段或位域 在前面已經提起過,在計算機中是采用二進制0和1來表示數據的,每一個0或者1占用1位(bit)存儲空間,8位組成一個字節(byte),為計算機中數據類型的最小單位,如char在32bit系統中占用一個字節。但是正如我們知道的,有時候程序中的數據可能并不需要這么的字節,比如一個開關的狀態,只有開和關,用1和0分別替代就可以表示。此時開關的狀態只需要一位存儲空間就可以滿足要求。如果用一個字節來存儲,顯然浪費了另外的7位存儲空間。所以在C語言中就有了位段(有的也叫位域,其實是一個東西)這個概念。具體的語法就是在變量名字后面,加上冒號(:)和指定的存儲空間的位數。具體的定義語法如下:1: struct 位段名稱 2: { 3: 位段數據類型 位段變量名稱 : 位段長度, 4: ....... 5: } 6: 7: 實例 8: 9: struct Node 10: { 11: char a:2; 12: double i; 13: int c:4; 14: }node; 其實定義很簡單,上面示例的意義是,定義一個char變量a,占用2位存儲空間,一個double變量i,以及一個占用4位存儲的int變量c。請注意這里改變了變量本來占用字節的大小,并不是我們常規定義的一個int變量占用4個字節,一個char變量占用1一個字節。但是sizeof(node) = ?呢,在實際的運行環境中運行,得到sizeof(node) = 24;為什么呢?說起來其實也很簡單,字節對齊,什么是字節對齊,待會下一個段落會具體講解。先來看一個面試示例,代碼如下: 1: #include 2: 3: using namespace std; 4: 5: union 6: { 7: struct 8: { 9: char i:1; 10: char j:2; 11: char m:3; 12: }s; 13: 14: char ch; 15: }r; 16: 17: int _tmain(int argc, _TCHAR* argv[]) 18: { 19: r.s.i = 1; 20: r.s.j = 2; 21: r.s.m = 3; 22: 23: cout<<" r.ch = "<<(int)r.ch<<" = 0x"<<hex<<(int)r.ch<<endl 24: <<" sizeof(r) = "<<sizeof(r)<<endl; 25: 26: return 0; 27: } 好了,具體結果是怎么樣的呢? r.ch = 29 = 0x1d sizeof(r) = 1 為什么是這個結果?說起來其實也很簡單,結合前面的大小端,可以具體來分析下,先看下表: m:3 j:2 i:1 7 6 5 4 3 2 1 0 0 0 0 1 1 1 0 1 1 D 上面的表格,解釋了為什么這里等于29=0x1D。首先i、j、m分別占用1、2、3位,分布在一個字節中。故根據賦值語句可知,在內存的相應的字節上首先存儲i=1,然后存儲j=2,也即10,而后是m=3,也即011。可看上表的不同顏色所示,然后不足的位,補0來填充。所以整個字節就是0x1D=29,顧r.ch = 29 = 0x1D。 關于位段,補充以下規則:三、內存對齊 內存地址對齊,是一種在計算機內存中排列數據(表現為變量的地址)、訪問數據(表現為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就只需要一次就可以讀取到,這樣的話就加快讀取效率。 在這里給出三個個人認為講解比較好的網址,供大家參考,英語比較好的朋友,推薦閱讀。 a、Data structure alignment(http://en.wikipedia.org/wiki/Data_structure_alignment) b、Data alignment: Straighten up and fly right(http://www.ibm.com/developerworks/library/pa-dalign/) c、About Data Alignment(http://msdn.microsoft.com/zh-cn/library/ms253949.aspx) 1、基本數據對齊 在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對齊(注:在Linux平臺下是4-byte對齊,超過4-byte都是以4-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對齊。 2、結構體數據對齊 結構體數據對齊,是指結構體內的各個數據對齊。在結構體中的第一個成員的首地址等于整個結構體的變量的首地址,而后的成員的地址隨著它聲明的順序和實際占用的字節數遞增。為了總的結構體大小對齊,會在結構體中插入一些沒有實際意思的字符來填充(padding)結構體。1: #include 2: 3: using namespace std; 4: 5: union 6: { 7: struct 8: { 9: char i:1; 10: char j:2; 11: char m:3; 12: }s; 13: 14: char ch; 15: }r; 16: 17: struct 18: { 19: char i; 20: double j; 21: int m; 22: }node; 23: 24: int _tmain(int argc, _TCHAR* argv[]) 25: { 26: r.s.i = 1; 27: r.s.j = 2; 28: r.s.m = 3; 29: 30: node.i = 'm'; 31: node.j = 3.1415926; 32: node.m = 100; 33: 34: cout<<" r.ch = "<<(int)r.ch<<" = 0x"<<hex<<(int)r.ch<<endl 35: <<" sizeof(r) = "<<sizeof(r)<<endl; 36: 37: cout.unsetf(ios::hex); 38: 39: cout<<" sizeof(node) = "<<sizeof(node)<<endl; 40: 41: system("PAUSE"); 42: 43: return 0; 44: } 45: 運行結果截圖如下: 通過上面的運行截圖,可以得知,sizeof(r) = 1,sizeof(node)=24;首先看r存儲在首地址0x0111a150,node的首地址 為0x0111a138,這個符合先定義先分配空間,并且是從內存地址大到小的順序來分配空間。它們之間相差(0x0111a150-0x0111a138)=0x18=24byte(注意這里是16進制計算,借1當16,不是習慣性的當10)。而且通過每個成員的地址也知道,m和j之間隔8字節,double是占用8字節,j和i之間也是8字節,但是char只占用了一個字節,其余相差的7個字節使用0來填充。同樣int成員在后面的4個高字節中也填充了0,以滿足8字節對齊(前面4個字節按小端高字節高地址低字節低地址存放)。同樣r只占用0x0111a150這個字節,值為0x1d=29。 在結構體中,成員數據對齊滿足以下規則: a、結構體中的第一個成員的首地址也即是結構體變量的首地址。 b、結構體中的每一個成員的首地址相對于結構體的首地址的偏移量(offset)是該成員數據類型大小的整數倍。 c、結構體的總大小是對齊模數(對齊模數等于#pragma pack(n)所指定的n與結構體中最大數據類型的成員大小的最小值)的整數倍。 在看另外一個示例,來理解以上規則。1: #include "stdafx.h" 2: 3: #include 4: 5: using namespace std; 6: 7: struct 8: { 9: char a; 10: int b; 11: short c; 12: char d; 13: }dataAlign; 14: 15: struct 16: { 17: char a; 18: char d; 19: short c; 20: int b; 21: 22: }dataAlign2; 23: 24: int _tmain(int argc, _TCHAR* argv[]) 25: { 26: dataAlign.a = 'A'; 27: dataAlign.b = 0x12345678; 28: dataAlign.c = 0xABCD; 29: dataAlign.d = 'B'; 30: 31: dataAlign2.a = 'A'; 32: dataAlign2.b = 0x12345678; 33: dataAlign2.c = 0xABCD; 34: dataAlign2.d = 'B'; 35: 36: cout<<" sizeof(dataAlign) = "<<sizeof(dataAlign)<<endl; 37: cout<<" sizeof(dataAlign2) = "<<sizeof(dataAlign2)<<endl; 38: 39: system("PAUSE"); 40: 41: return 0; 42: } 運行結果截圖如下: 仔細觀察,會發現雖然是一樣的數據類型的成員,只不過聲明的順序不同,結構體占用的大小也不同,一個8-byte一個12-byte。為什么這樣,下面進行具體分析。 首先來看dataAlign2,第一個成員的地址等于結構體變量的首地址,第二個成員char類型,為了滿足規則b,它相對于結構體的首地址的偏移量必須是char=1的倍數,由于前面也是char,故不需要在第一個和第一個成員之間填充,直接滿足條件。第三個成員short=2如果要滿足規則b,也不需要填充,因為它的偏移量已經是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,為了滿足規則b,需要在第一個成員之后填充3-byte,讓它相對于結構體首地址偏移量為4,結合運行結果截圖,可知&dataAlign.a = 0x01109140,而&dataAlign.b = 0x01109144,它們之間相隔4-byte,0x01109141~0x01109143三個字節被0填充。第三個成員short=2,無需填充滿足規則b。第四個成員char=1,也不需要填充。OK,結構體總大小相加4 + 4 + 2 + 1 = 11。同樣最后需要驗證規則c,結構體中數據類型大小最大為第二個成員int=4,比VC默認對齊模數8小,故這個結構體的對齊模數仍然為4,顯然11 mode 4 != 0,故為了滿足規則c,需要在char后面填充一個字節,這樣結構體變量dataAlign的總大小為4 + 4 + 2 + 2 = 12。 好了,再來看看位段(也叫位域)這種數據類型在內存中的對齊。一個位域必須存儲在同一個字節中,不能跨字節,比如跨兩個字節。如果一個字節所剩空間不夠存儲另一位位域時,應該從下一個字節存放該位域。在滿足成員數據對齊的規則下,還滿足如下規則: d、如果相鄰位域類型相同,并且它倆位域寬度之和小于它的數據類型大小,則后面的字段緊鄰前面的字段存儲。 e、如果相鄰位域類型相同,但是它倆位域寬度之和大于它的數據類型大小,則后面的字段將從新的存儲單元開始,其偏移量為其類型的整數倍。 f、如果相鄰位域類型不同,在VC中是不采取壓縮方式,但是GCC會采取壓縮方式。 具體的結合下面示例來理解,具體代碼為: 1: #include 2: 3: using namespace std; 4: 5: struct 6: { 7: char a:4; 8: int b:6; 9: }bitChar2; 10: 11: struct 12: { 13: char a:3; 14: char b:3; 15: char c:7; 16: double d; 17: int e:4; 18: int f:30; 19: }bitChar; 20: 21: int _tmain(int argc, _TCHAR* argv[]) 22: { 23: bitChar2.a = 7; 24: bitChar2.b = 32; 25: cout<<" sizeof(bitChar2) = "<<sizeof(bitChar2)<<endl; 26: 27: bitChar.a = 6; 28: bitChar.b = 4; 29: bitChar.c = 45; 30: bitChar.d = 100.0; 31: bitChar.e = 7; 32: bitChar.f = 0x12345678; 33: cout<<"sizeof(bitChar) = "<<sizeof(bitChar)<<endl; 34: 35: system("PAUSE"); 36: return 0; 37: } 運行結果截圖如下: 首先來分析bitChar2,因為滿足規則f,在VC下不壓縮,同時要滿足規則a、b、c。所以第二個成員需要最低偏移量為4,第一個成員后需要填充3-byte。再看第二個bitChar,首先成員a、b滿足規則d,故需要填充在0x00ab9138這個字節內,具體存儲順序見下圖: b:3 a:3 7 6 5 4 3 2 1 0 0 0 1 0 0 1 1 0 2 6 而第二個成員和第三個成員滿足規則e,位域之和大于sizeof(char)=1的大小,所以需要一個偏移量。而第四個成員double=8為了滿足規則b,必須在第三個成員之后填充6-byte,滿足最小偏移量8。第五個成員不需要偏移,故無需填充。而第六個成員和第五個成員滿足規則e,所以需要從新的存儲單元開始存儲,偏移量為int=4的整數倍,然后存儲最后的成員e,中間需要填充3-byte。 由此可以得出總的大小為1 + 1 + 6 + 8 + 4 + 4 = 24,滿足規則c,即是24 mode 8 = 0。四,關于#pragma pack(n) #pragma pack(push) //保存對齊狀態 #pragma pack(n) /設置對齊模數(選擇n和一般情況下選出來的模數的較小者做對齊模數) #pragma pack(pop) //恢復對齊狀態
總結
以上是生活随笔為你收集整理的大小端、位段和内存对齐的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Chrome性能分析工具Coverage
- 下一篇: Annovar 信息注释