閱讀(一)git
咱們經常看到「alignment", "endian"之類的字眼, 但不多有C語言教材提到這些概念. 實際上它們是與處理器與內存接口, 編譯器類型密切相關的.程序員
考慮這樣一個例子: 兩個異構的CPU進行通訊, 定義了這樣一個結果來傳遞消息:算法
struct Message {
用這樣一個結構來傳遞消息貌似很是方便,但也引起了這樣一個問題: 若這兩種不一樣的CPU對該結構的定義不同,二者就會對消息有不一樣的理解.有可能致使二義性.會引起二義性的有這兩個方面:short opcode; char subfield; long message_length; char version; short destination_processor; }message;
本文先介紹內存地址對齊和大小端的概念, 再回頭來看這個例子就豁然開朗了.小程序
內存地址對齊,洋名叫作" Byte Alignment".windows
大部分16位和32位的CPU不容許將字或者長字存儲到內存中的任意地址. 好比Motorola 68000不容許將16位的字存儲到奇數地址中, 將一個16位的字寫到奇數地址將引起異常.數組
實際上, 對於c中的字節組織, 有這樣的對齊規則:緩存
不一樣CPU的對其規則可能不一樣, 請參考手冊. |
爲何會有上述的限制呢? 理解了內存組織, 就會清楚了
CPU經過地址總線來存取內存中的數據, 32位的CPU的地址總線寬度即爲32位, 標記爲A[0:31]. 在一個總線週期內, CPU從內存讀/寫32位. 可是CPU只能在可以被4整除的地址進行內存訪問, 這是由於: 32位CPU不使用地址總線的A1和A0,好比ARM, 它的A[0:1]用於字節選擇(32位總線,一次傳輸一共能夠傳輸4byte數據,A[0:1]能夠控制4byte數據中第幾byte傳輸或者所有傳輸), 用於邏輯控制, 而不和存儲器相連, 存儲器鏈接到A[2:31].
訪問內存的最小單位是字節(byte), A0和A1不使用, 那麼對於地址來講, 最低兩位是無效的, 因此它只能識別能被4整除的地址了. 在4字節中, 經過A0和A1肯定某一個字節.
再看看剛纔的message結構, 你想一想它佔了多少字節? 別想固然的覺得是10個字節. 實際上它佔了12個字節. 不信? 用sizeof(message)看吧. 對於結構體, 編譯器會針對起中的元素添加"pad"以知足字節對齊規則. message會被編譯器改成下面的形式:
struct Message網絡
{ short opcode; char subfield; char pad1; // Pad to start the long word at a 4 byte boundary long message_length; char version; char pad2; // Pad to start a short at a 2 byte boundary short destination_processor; char pad3[4]; // Pad to align the complete structure to a 16 byte boundary };
若是不一樣的編譯器採用不一樣的對齊規則, 對傳遞message可就麻煩了.
數據結構
Byte Endian是指字節在內存中的組織,因此也稱它爲Byte Ordering. 架構
對於數據中跨越多個字節的對象, 咱們必須爲它創建這樣的約定:
(1) 它的地址是多少? (2) 它的字節在內存中是如何組織的? |
針對第一個問題,有這樣的解釋:
對於跨越多個字節的對象,通常它所佔的字節都是連續的, 它的地址等於它所佔字節最低地址.(鏈表多是個例外, 但鏈表的地址可看做鏈表頭的地址).
好比: int x, 它的地址爲0x100. 那麼它佔據了內存中的Ox100, 0x101, 0x102, 0x103這四個字節. |
上面只是內存字節組織的一種狀況: 多字節對象在內存中的組織有通常有兩種約定. 考慮一個W位的整數. 它的各位表達以下:
[Xw-1, Xw-2, ... , X1, X0] |
它的MSB (Most Significant Byte, 最高有效字節)爲[Xw-1, Xw-2, ... Xw-8]; LSB (Least Significant Byte, 最低有效字節)爲 [X7, X6, ..., X0]. 其他的字節位於MSB, LSB之間.
LSB和MSB誰位於內存的最低地址, 即誰表明該對象的地址? 這就引出了大端(Big Endian)與小端(Little Endian)的問題。
若是LSB在MSB前面, 既LSB是低地址, 則該機器是小端; 反之則是大端. DEC (Digital Equipment Corporation, 如今是Compaq公司的一部分)和Intel的機器通常採用小端. IBM, Motorola, Sun的機器通常採用大端. 固然, 這不表明全部狀況. 有的CPU即能工做於小端, 又能工做於大端, 好比ARM, PowerPC, Alpha. 具體情形參考處理器手冊.
舉個例子來講名大小端: 好比一個int x, 地址爲0x100, 它的值爲0x1234567. 則它所佔據的0x100, 0x101, 0x102, 0x103地址組織以下圖:
0x01234567的MSB爲0x01, LSB爲0x67. 0x01在低地址(或理解爲"MSB出如今LSB前面,由於這裏討論的地址都是遞增的), 則爲大端; 0x67在低地址則爲小端.
認清這樣一個事實: C中的數據類型都是從內存的低地址向高地址擴展,取址運算"&"都是取低地址. |
兩個測試Bit Endian的小程序
method_1
#include <stdio.h> |
int c 在內存中的表達爲: 0x00000001. (這裏假設int爲4字節). 用char能夠截取一個字節. LSB爲0x01, 若它出如今c的低地址, 則爲小端.
method_2
#include <stdio.h> int main(void) { /* Each component to a union type is allocated storage at the beginning of the union */ union { short n; char c[sizeof(short)]; }un; un.n = 0x0102; if ((un.c[0] == 1 && un.c[1] == 2)) printf("big endian\n"); else if ((un.c[0] == 2 && un.c[1] == 1)) printf("little endian\n"); else printf("error!\n"); return 0; } |
union中元素的起始地址都是相同的——位於聯合的開始. 用char來截取感興趣的字節.
區分大端與小端有什麼用呢? 若是兩個不一樣Endian的機器進行通訊時, 就有必要區分了![]() |
閱讀(二)
1、什麼是對齊,以及爲何要對齊:
1. 現代計算機中內存空間都是按照byte劃分的,從理論上講彷佛對任何類型的變量的訪問能夠從任何地址開始,但實際狀況是在訪問特定變量的時候常常在特定的內存地址訪問,這就須要各種型數據按照必定的規則在空間上排列,而不是按順序的一個接一個的排放,這就是對齊。
2. 對齊的做用和緣由:各個硬件平臺對存儲空間的處理上有很大的不一樣。一些平臺對某些特定類型的數據只能從某些特定地址開始存取。其餘平臺可能沒有這種狀況, 可是最多見的是若是不按照適合其平臺的要求對數據存放進行對齊,會在存取效率上帶來損失。好比有些平臺每次讀都是從偶地址開始,若是一個int型(假設爲 32位)若是存放在偶地址開始的地方,那麼一個讀週期就能夠讀出,而若是存放在奇地址開始的地方,就可能會須要2個讀週期,並對兩次讀出的結果的高低 字節進行拼湊才能獲得該int數據。顯然在讀取效率上降低不少。這也是空間和時間的博弈。
2、對齊的實現
一般,咱們寫程序的時候,不須要考慮對齊問題。編譯器會替咱們選擇適合目標平臺的對齊策略。固然,咱們也能夠通知給編譯器傳遞預編譯指令而改變對指定數據的對齊方法。
可是,正由於咱們通常不須要關心這個問題,因此由於編輯器對數據存放作了對齊,而咱們不瞭解的話,經常會對一些問題感到迷惑。最多見的就是struct數據結構的sizeof結果,出乎意料。爲此,咱們須要對對齊算法所瞭解。
對齊的算法:
因爲各個平臺和編譯器的不一樣,現以本人使用的gcc version 3.2.2編譯器(32位x86平臺)爲例子,來討論編譯器對struct數據結構中的各成員如何進行對齊的。
設結構體以下定義:
struct A {
int a;
char b;
short c;
};
結構體A中包含了4字節長度的int一個,1字節長度的char一個和2字節長度的short型數據一個。因此A用到的空間應該是7字節。可是由於編譯器要對數據成員在空間上進行對齊。
因此使用sizeof(strcut A)值爲8。
如今把該結構體調整成員變量的順序。
struct B {
char b;
int a;
short c;
};
這時候一樣是總共7個字節的變量,可是sizeof(struct B)的值倒是12。
下面咱們使用預編譯指令#pragma pack (value)來告訴編譯器,使用咱們指定的對齊值來取代缺省的。
#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。
對於char型數據,其自身對齊值爲1,對於short型爲2,對於int,float,double類型,其自身對齊值爲4,單位字節。
這裏面有四個概念值:
1)數據類型自身的對齊值:就是上面交代的基本數據類型的自身對齊值。
2)指定對齊值:#pragma pack (value)時的指定對齊值value。
3)結構體或者類的自身對齊值:其成員中自身對齊值最大的那個值。
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;
同理,分析上面例子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,因此順序存放在0x000二、0x000三、0x000四、0x0005四個連續字節中,符合0x0002 % 2 = 0。第三個變量c的自身對齊值爲2,因此有效對齊值爲2,順序存放
在0x000六、0x0007中,符合0x0006 % 2 = 0。因此從0x0000到0x00007共八字節存放的是C的變量。又C的自身對齊值爲4,因此C的有效對齊值爲2。又8 % 2 = 0,C只佔用0x0000到0x0007的八個字節。因此sizeof(struct C) = 8.
有 了以上的解釋,相信你對C語言的字節對齊概念應該有了清楚的認識了吧。在網絡程序中,掌握這個概念但是很重要的喔,在不一樣平臺之間(好比在Windows 和Linux之間)傳遞2進制流(好比結構體),那麼在這兩個平臺間必需要定義相同的對齊方式,否則莫名其妙的出了一些錯,但是很難排查的哦^_^。
閱讀(三)
內存地址對齊,是一種在計算機內存中排列數據、訪問數據的一種方式,包含了兩種相互獨立又相互關聯的部分:基本數據對齊和結構體數據對齊。當今的計算機在計算機內存中讀寫數據時都是按字(word)大小塊來進行操做的(在32位系統中,數據總線寬度爲32,每次能讀取4字節,地址總線寬度爲32,所以最大的尋址空間爲2^32=4GB,可是最低2位A[0],A[1]是不用於尋址,A[2-31]才能存儲器相連,所以只能訪問4的倍數地址空間,可是總的尋址空間仍是2^30 * 字長 = 4GB,所以在內存中全部存放的基本類型數據的首地址的最低兩位都是0,除結構體中的成員變量)。基本類型數據對齊就是數據在內存中的偏移地址必須等於一個字的倍數,按這種存儲數據的方式,能夠提高系統在讀取數據時的性能。爲了對齊數據,可能必須在上一個數據結束和下一個數據開始的地方插入一些沒有用處字節,這就是結構體數據對齊。
舉個例子,假設計算機的字大小爲4個字節,所以變量在內存中的首地址都是知足4地址對齊,CPU只能對4的倍數的地址進行讀取,而每次能讀取4個字節大小的數據。假設有一個整型的數據a的首地址不是4的倍數(以下圖所示),不妨設爲0X00FFFFF3,則該整型數據存儲在地址範圍爲0X00FFFFF3~0X00FFFFF6的存儲空間中,而CPU每次只能對4的倍數內存地址進行讀取,所以想讀取a的數據,CPU要分別在0X00FFFFF0和0X00FFFFF4進行兩次內存讀取,並且還要對兩次讀取的數據進行處理才能獲得a的數據,而一個程序的瓶頸每每不是CPU的速度,而是取決於內存的帶寬,由於CPU得處理速度要遠大於從內存中讀取數據的速度,所以減小對內存空間的訪問是提升程序性能的關鍵。從上例能夠看出,採起內存地址對齊策略是提升程序性能的關鍵。
結構體(struct)是C語言中很是有用的用戶自定義數據類型,而結構體類型的變量以及其各成員在內存中的又是怎樣佈局的呢?怎樣對齊的呢?很顯然結構體變量首地址必須是4字節對齊的,可是結構體的每一個成員有各自默認的對齊方式,結構體中各成員在內存中出現的位置是隨它們的聲明順序依次遞增的,而且第一個成員的首地址等於整個結構體變量的首地址。下面列出了在Microsoft,Borland,GNU上對於X86架構32位系統的結構體成員各類類型的默認對齊方式。
char(1字節),1字節對齊
short(2字節),2字節對齊
int(4字節),4字節對齊
float(4字節),4字節對齊
double(8字節),Windows系統中8字節對齊,Linux系統中4字節對齊。
當結構體某一成員後面緊跟一個要求比較大的地址對齊成員時(例如char成員變量後面跟一個double成員變量),這時要插入一些沒有實際意義的填充(Padding)。並且總的結構體大小必須爲最大對齊的倍數。
下面是一個有char,int,short三種類型,4個成員組成的結構體,該結構體在還未編譯以前是大小佔8個字節。
struct AlignData{
char a;
short b;
int c;
char d;
};
編譯以後,爲了保持結構體中的每一個成員都是按照各自的對齊,編譯器會在一些成員之間插入一些padding,所以編譯後獲得以下的結構體:
struct AlignData {
char a;
char Padding0[1];
short b;
int c;
char d;
char Padding1[3];
};
編譯後該結構體的大小爲12個字節,最後一個成員d後面填充的字節數要使該結構體的總大小是其成員類型中擁有最大字節數的倍數(int擁有最大字節數),所以d後面要填充3個字節。下面舉一些結構體例子來講明結構體的填充方式:
例子1:
struct struct1{
char a1;
char b1;
};
結構體struct1的大小爲2字節,由於char在結構體中的默認對齊是1,所以在a1和b1之間沒有數據填充,並且其成員中佔用字節最大的類型爲char,所以結構體結束處和b1之間也沒有數據填充。
例子2:
struct struct2{
char a2;
short b2;
};
結構體struct2的大小爲4字節,b2的是按2字節對齊,所以在b2於a2之間填充一個字節,而其成員中佔用字節最大的類型爲short,所以該結構體結束處和b2之間沒有任何數據填充。
例子3:
Struct struct3{
double a3;
char b3;
};
結構體struct3的大小爲16字節,由於b3是按1字節對齊,因此b3與a3之間沒有數據填充,而其成員中佔用字節最大的類型爲double,在Windows平臺下是8字節對齊,所以該結構體結束處和b3之間有7個字節的數據填充。
填充字節的大小和新的偏移地址有以下計算公式:
padding = (align - (offset mod align)) mod align
new offset = offset + ((align - (offset mod align)) mod align)
例如求成員a,b之間的填充字節,b的默認對齊爲align=2個字節,b的未填充以前的偏移量offset=1,所以填充字節數padding=(2-(1 mod 2)) mod 2 = 1字節。若是要算接下來的成員之間的填充數,已經填充的字節也要算上,否則在算偏移量的時候會出錯編譯後的結構體比未編譯以前多出了3個字節,有沒有什麼辦法能夠在保持各成員地址對齊的前提下,又能減小結構體的大小?必須有的!
若是把struct AlignData的成員順序調整成以下形式:
struct AlignData{
char a;
char d;
short b;
int c;
};
那麼編譯後不用填充字節就能保持全部的成員都按各自默認的地址對齊。這樣能夠節約很多內存!通常的結構體成員按照默認對齊字節數遞增或是遞減的順序排放,會使總的填充字節數最少。基本數據類型數組在內存中的佈局並非每一個數組的元素都是按照4字節對齊的,可是數組的首地址必須是按照4字節對齊,並且每一個元素之間沒有填充,爲何沒有填充呢?地址對齊和填充的目的是減小內存讀取的次數,但如今只要數組的首地址按4字節對齊,任何小於等於4字節的類型數組(char, short, int)中的任意數組元素都能經過一次內存讀取來得到(假設該數據沒有加載到高速緩存),任何大於4字節類型數組(double)中的任意數組元素都能經過兩次內存讀取來得到任何大於4字節類型數組(double)中的任意數組元素都能經過兩次內存讀取來得到。所以要求每一個數組元素都是按照4字節對齊是沒有必要,浪費空間的。
結構體數組在內存中的佈局,只要保持結構體數組的首地址是按照4字節對齊,並且每一個數組元素一樣也沒必要按照4字節地址對齊,就能儘可能使內存的讀取次數降到最低,由於只要每一個結構體元素本身內部的填充和對齊都是上述的方式,那麼一樣也能達到既能減小內存訪問的次數,又能節約沒必要要的內存浪費。可是有人會有這樣的疑問,既然每一個結構體首地址按照4字節對齊,爲何結構體內部每種數據類型還要各自默認的對齊大小進行對齊?其實其目的一樣也是減小內存訪問的次數,由於結構體是用戶自定義的類型,內部仍是由一些基本數據類型組成的!以上的對齊方式都是Windows默認的對齊方式,用戶能夠根據需求來設置本身的對齊方式,特別是在一些內存受限的系統中,內存比速度更重要!可是建議用戶仍是不要輕易來設置本身的對齊方式,若是用得不恰當的話,可能會形成大量冗餘的內存讀取,並且可能會出現不兼容的問題。能夠用#pragma pack指令來對其進行設置。由內存地址對齊而引起的對減小內存訪問次數的思考當今的CPU的處理速度遠比內存訪問的速度快,程序的執行速度的瓶頸每每不是CPU的處理速度不夠,而是內存訪問的延遲,雖然當今CPU中加入了高速緩存用來掩蓋內存訪問的延遲,可是若是高密集的內存訪問,一種延遲是無可避免的。內存地址對齊給程序帶來了很大的性能提高,在windows等系統了,編譯器都提供了自動地址對齊,給程序員帶來了很大的方便。可是減小對內存訪問仍是值得探討的問題。
調整結構體成員變量的佈局是減小內存訪問次數的途徑之一。下面分別介紹兩種不一樣的結構體數據成員調整方案,都能獲得很好的性能提高。
1. 按成員內存對齊大小按升序或是降序排序,減小結構體的大小。看以下兩個結構體:
struct BeforeAdjust{
char a;
short b;
int c;
char d;
};
struct AfterAdjust{
char a;
char d;
short b;
int c;
};
從表面上看結構體BeforeAdjust和AfterAdjust成員都同樣,就是成員佈置的順序有差別,所以形成了這兩種類型數據佔據空間大小有所不一樣,BeforeAdjust大小佔12個字節,AfterAdjust大小佔8個字節,所以從讀取一個BeforeAdjust類型的數據要進行3次內存讀取操做,而AfterAdjust類型的數據要進行2次內存讀取操做。下面我分別對大小爲1000萬的這兩種結構體的動態數組進行初始化,而後依次讀取數組數據對每一個數據成員作求和操做,獲得的測試時間以下表。
從上面的測試數據能夠看出,一樣的數據成員,就是由於擺放的順序不一樣而形成性能有28.127%的差別。所以調整好結構體內的數據成員的擺放順序既能夠減小內存的使用,又能夠提升程序的性能。
2. 把一些字節數佔用比較少的成員合併到字節數佔用大的成員。首先看以下兩個結構體:
struct UnMergeMember{
int a;
int b;
char c;
};
struct MergeMember{
int a;
union{
int b;
char c;};
};
UnMergeMember結構體由三個成員變量a,b,c,分別是int,int,char類型,按照地址對齊的規則,該結構體佔用12個字節。所以初始化UnMergeMember類型變量涉及到3次內存讀操做,3次賦值操做,3次內存寫操做。MergeMember結構體由一個int類型的成員和一個聯合體變量組成,按照地址對齊規則,該結構體佔用8個字節。
聯合體union{int b; char c;}佔用4個字節,高位3個字節保存變量b(前提是用3字節能足夠表示b的數據範圍),最低位1個字節保存變量c。假設定義一個MergeMember類型的變量爲merge,初始化每一個成員變量以下:
merge.a = some integer;
merge.b = some integer;
merge.b <<= 8;
merge.c = some char;
初始化一個MergeMember類型的數據只涉及到2次的內存讀操做、3次賦值操做、1次位移操做,2次內存寫操做。從上述能夠看出初始化一個UnMergeMember類型的變量比MergeMember類型變量多了1次讀操做和寫操做,少了1次位移操做。下面我分別對大小爲1000萬的這兩種結構體的動態數組進行初始化,而後依次讀取數組數據對每一個數據成員作求和操做,獲得的測試時間以下表。
從上面的測試數據能夠看出,在結構體中把小數據歸併到大數據能夠減小內存讀取的次數,雖然多了一些CPU的操做,可是用CPU的操做換取內存數據讀取次數,程序性能確定能獲得提升。上面的測試程序能夠獲得43.5%的性能提升(基本等於內存讀取次數減小比(6-4)/4=50%),在對性能要求特別高的系統中,這麼大幅度的性能是至關可觀。上述的例子也能夠經過位段實現(Bit-fields),可是位段只能對整數進行操做,若是把浮點數於和int類型的數據放在一塊兒用位段實現顯然不行,可是經過位移的方法也能夠把char類型的數據併入浮點數float或是double中。
3. 經過位段(Bit-fields)的方式把一些整形數據按照各自需求的字段數來分配。這種方式能夠大大節省空間,TCP協議的首部的定義就是採用位段的方式來定義的。位段的使用比較簡單,這裏我就不贅述了,能夠參考相關的資料。可是值得注意的是,使用位段的方式的對齊方式也要遵照上述結構體對齊的方式,看下面一些結構體以及相應的大小:
struct BitField1{
char a:1;
char b:2;
char c:3;
char d:2;
};
struct BitField2{
char a:1;
char b:2;
char c:3;
char d:2;
int e:4;
};
Sizeof(BitField1)等於1(char大小的倍數),sizeof(BitField2)等於8(int大小的倍數)。