顶级c程序员之路 选学篇-1 深入理解字节,字节序与字节对齐
?
?????????????????????????深入理解字節(jié),字節(jié)序與字節(jié)對齊
一?總述
???作為一個職業(yè)的coder玩家,首先應(yīng)該對計算機的字節(jié)有所了解。
???我們經(jīng)常談到的2進(jìn)制流,字節(jié)(字符)流,數(shù)據(jù)類型流(針對編程),結(jié)構(gòu)流等說法,2進(jìn)制流,0和1的操作,屬于cpu級。從字符流向上都是我們玩家關(guān)心,字節(jié)流屬于操作系統(tǒng)級。今天談的就是字節(jié)流操作。
?
二?字節(jié)
???因為計算機用二進(jìn)制,所以希望基本存儲單位的是2的n次方(應(yīng)該和硬件有關(guān))。??這樣讀取字節(jié)的時候,開銷不會太高,可以達(dá)到最大性能,因為剛開始,計算機是美國發(fā)明的,西文字符(英文字母大小寫,數(shù)字,其他特殊字符等將近有1百多個,所以用7位來表示,這樣可以把所有西文字符表達(dá)完,再加上一位校檢位,一共8位,由于ASCⅡ的廣泛應(yīng)用,所有后來,一個字節(jié)占8位就成了國際規(guī)定的標(biāo)準(zhǔn)了,一直沿用至今(有待研究,但不是今天的主題)。
???一個字節(jié)占8個2進(jìn)制位,數(shù)據(jù)類型流,就是在c語言里面的數(shù)據(jù)類型占多少個字節(jié)。然后直接操作數(shù)據(jù)類型。在目前的32位系統(tǒng)中,c語言的基本數(shù)據(jù)類型有以下幾種:
Char??占一個字節(jié)?(-2^7?-?2^7-1?,-128?到?127)最高位?為符號位
Unsigned?char?占一個字節(jié)???(0-2^8,0到255)
Short???占2個字節(jié)?(-2^15?-?2^15-1?,?-32768到32767)最高位?為符號位
Unsigned?short?占2個字節(jié)?(0?-?2^16?,?0到65536)
Int?(字長,對于32位機為32位,16位機為16位,長度不固定,和系統(tǒng)平臺有關(guān),處理器位數(shù)有關(guān),代表尋址空間),在32位機占4個字節(jié)(-2^31-2^31?)最高位?為符號位
Unsigned??int?占4個字節(jié)(0-2^32?)
Long?int?為4個字節(jié),在16,32位機都占4個字節(jié)(-2^31-2^31?)最高位?為符號位
Unsigned??long?int占4個字節(jié)(0-2^32?)
sizeof(short)?<=?sizeof(int)?<=?sizeof(long)??
在32位機?int和long都是32位,沒有什么大的區(qū)別,但還是有些小的區(qū)別,有時最好用long,他大小固定,如果到其他平臺,比如64位,他還是占4個字節(jié),而int卻占8個字節(jié),可以增加代碼的可移植性。
Long?long?int?占8個字節(jié)(c99標(biāo)準(zhǔn))??
Unsigned?long?long?int?占8個字節(jié)(c99標(biāo)準(zhǔn))??
浮點類型?由于浮點類型和整形的編碼不一樣,所以浮點型需要特殊分析。
Float?占4個字節(jié) ?
Double?占8個字節(jié)?
深入理解浮點類型,請看這邊文章:
http://blog.csdn.net/caotiancool/archive/2005/04/29/368375.aspx?
主要是這不是這篇文章的重點。
進(jìn)入今天的主題
三?字節(jié)序
??為什么有字節(jié)序這個概念存在呢?
不同的CPU有不同的字節(jié)序類型?這些字節(jié)序是指整數(shù)在內(nèi)存中保存的順序?這個叫做主機序?,就是多個字節(jié)在內(nèi)存中擺放位置順序和解釋順序。
最常見的有兩種?:
1.?Little?endian:將低序字節(jié)存儲在起始地址?4321
2.?Big?endian:將高序字節(jié)存儲在起始地址????1234
以前還有什么Middle?endian,就亂序,也被淘汰。
LE?little-endian?
最符合人的思維的字節(jié)序?
地址低位存儲值的低位?
地址高位存儲值的高位?
怎么講是最符合人的思維的字節(jié)序,是因為從人的第一觀感來說?
低位值小,就應(yīng)該放在內(nèi)存地址小的地方,也即內(nèi)存地址低位?
反之,高位值就應(yīng)該放在內(nèi)存地址大的地方,也即內(nèi)存地址高位?
BE?big-endian?
最直觀的字節(jié)序?
地址低位存儲值的高位?
地址高位存儲值的低位?
為什么說直觀,不要考慮對應(yīng)關(guān)系?
只需要把內(nèi)存地址從左到右按照由低到高的順序?qū)懗?
把值按照通常的高位到低位的順序?qū)懗?
兩者對照,一個字節(jié)一個字節(jié)的填充進(jìn)去?
例子:在內(nèi)存中雙字0x01020304(DWORD)的存儲方式?
內(nèi)存地址?
4000?4001?4002?4003?
LE?04?03?02?01?
BE?01?02?03?04?
例子:如果我們將0x1234abcd寫入到以0x0000開始的內(nèi)存中,則結(jié)果為
??????big-endian???little-endian
0x0000???0x12???????0xcd
0x0001???0x23???????0xab
0x0002???0xab???????0x34
0x0003???0xcd???????0x12
x86系列CPU都是little-endian的字節(jié)序.?
網(wǎng)絡(luò)字節(jié)順序是TCP/IP協(xié)議棧中規(guī)定好的一種數(shù)據(jù)表示格式,它與具體的CPU類型、操作系統(tǒng)等無關(guān),從而可以保證數(shù)據(jù)在不同主機之間傳輸時能夠被正確解釋。網(wǎng)絡(luò)字節(jié)順序采用big?endian排序方式。
為了進(jìn)行轉(zhuǎn)換?bsd?socket提供了轉(zhuǎn)換的函數(shù)?有下面四個
linux的源代碼(/include/netinet/in.h)
#?if?__BYTE_ORDER?==?__BIG_ENDIAN?
/*?The?host?byte?order?is?the?same?as?network?byte?order,?
???so?these?functions?are?all?just?identity.??*/?
#?define?ntohl(x)?(x)?
#?define?ntohs(x)?(x)?
#?define?htonl(x)?(x)?
#?define?htons(x)?(x)?
#?else?
#??if?__BYTE_ORDER?==?__LITTLE_ENDIAN?
#???define?ntohl(x)?__bswap_32?(x)?
#???define?ntohs(x)?__bswap_16?(x)?
#???define?htonl(x)?__bswap_32?(x)?
#???define?htons(x)?__bswap_16?(x)?
#??endif?
#?endif
htons?把unsigned?short類型從主機序轉(zhuǎn)換到網(wǎng)絡(luò)序
htonl?把unsigned?long類型從主機序轉(zhuǎn)換到網(wǎng)絡(luò)序
ntohs?把unsigned?short類型從網(wǎng)絡(luò)序轉(zhuǎn)換到主機序
ntohl?把unsigned?long類型從網(wǎng)絡(luò)序轉(zhuǎn)換到主機序
在使用little?endian的系統(tǒng)中?這些函數(shù)會把字節(jié)序進(jìn)行轉(zhuǎn)換?
在使用big?endian類型的系統(tǒng)中?這些函數(shù)會定義成空宏
?都是4個宏函數(shù):
?分析其中1個,htonl(x)我簡化為下:
??#define??htonl(x)??\??//連接符,連接下一行
??((unsigned?long?)?\
(?\
(((unsigned?long)(x)&0x000000ff<<24)|?\
(((unsigned?long)(x)&0x0000ff00)<<8)|?\
(((unsigned?long)(x)&0x00ff0000)>>8)|?\
(((unsigned?long)(x)&0xff000000)>>24)\
))
這樣寫是方便好看一點,宏的最后最好是用一個括號括起來,或者用do{}while(0)包起來。
這樣可以防止宏嵌套產(chǎn)生意想不到問題等,如果自己熟悉了就可以簡潔一點。上面那個宏的作用是把4321變成1234(字節(jié)擺放順序)。
一般c語言編寫程序的字節(jié)序都是系統(tǒng)相關(guān)的(java的字節(jié)碼是big-endian?和網(wǎng)絡(luò)字節(jié)序一樣,所以他和網(wǎng)絡(luò)通信不需要關(guān)心字節(jié)序問題),如果要和其他平臺進(jìn)行通信,都要進(jìn)行字節(jié)序轉(zhuǎn)換,跨平臺開發(fā)時也應(yīng)該注意保證只用一種字節(jié)序?不然兩方的解釋不一樣就會產(chǎn)生bug。
網(wǎng)絡(luò)上流傳一個測試自己系統(tǒng)是什么字節(jié)序函數(shù)代碼:
byte_type get_sys_byte_order()
{
?? ? union
?? ?{
?? ? ? ? int ?b;
?? ? ? ? char a[4];
?? ? }U;
?? ? U.b = 0x01;
?? ? if(0x01 == U.a[0] )
?? ? {
?? ? ? ? return ? little_endian_type;
?? ? ?}
?? ?else
?? ? ?{
?? ? ? ? return ? big_endian_type;
?? ? ?}?
}?
注:
1、網(wǎng)絡(luò)與主機字節(jié)轉(zhuǎn)換函數(shù):htons?ntohs?htonl?ntohl?(s?就是short?l是long?h是host?n是network)
2、不同的CPU上運行不同的操作系統(tǒng),字節(jié)序也是不同的,參見下表。
處理器?????????操作系統(tǒng)?????字節(jié)排序
Alpha????????????全部?????Little?endian
HP-PA?????????????NT??????Little?endian
HP-PA????????????UNIX?????Big?endian
Intelx86?????????全部?????Little?endian?<-----x86系統(tǒng)是小端字節(jié)序系統(tǒng)
Motorola680x()???全部?????Big?endian
MIPS??????????????NT??????Little?endian
MIPS?????????????UNIX?????Big?endian
PowerPC???????????NT??????Little?endian
PowerPC??????????非NT?????Big?endian???<-----PPC系統(tǒng)是大端字節(jié)序系統(tǒng)
RS/6000??????????UNIX?????Big?endian
SPARC????????????UNIX?????Big?endian
IXP1200?ARM核心??全部?????Little?endian?
四?字節(jié)對齊
首先我看哈程序的優(yōu)化種類:
Cpu級優(yōu)化((讀內(nèi)存),流水線,cache,現(xiàn)在多核等)->2進(jìn)制級(即01代碼)優(yōu)化(現(xiàn)在估計沒有人去做了)->匯編級(指令)優(yōu)化->高級程序里面的代碼級優(yōu)化(位運算,前++和后++,數(shù)組和指針,if?else和switch?case等優(yōu)化)->算法優(yōu)化(流程優(yōu)化)->軟件架構(gòu)級優(yōu)化......
??而現(xiàn)在我們討論的字節(jié)對齊屬于cpu級優(yōu)化,可以加速cpu讀取內(nèi)存時間。
什么是字節(jié)對齊:
現(xiàn)代計算機中內(nèi)存空間都是按照byte劃分的,從理論上講似乎對任何類型的變量的訪問可以從任何地址開始,但實際情況是在訪問特定類型變量的時候經(jīng)常在特定的內(nèi)存地址訪問,這就需要各種類型數(shù)據(jù)按照一定的規(guī)則在空間上排列,而不是順序的一個接一個的排放,這就是對齊。?
????對齊的作用和原因:各個硬件平臺對存儲空間的處理上有很大的不同。一些平臺對某些特定類型的數(shù)據(jù)只能從某些特定地址開始存取。比如有些架構(gòu)的CPU在訪問一個沒有進(jìn)行對齊的變量的時候會發(fā)生錯誤(比如高通平臺,一般的手機平臺都采用美國高通公司開發(fā)平臺,對于無線上網(wǎng)卡來說,現(xiàn)在都是多核,要么是arm9+arm11,要么是arm9+2個Qdsp(Q是表示高通的dsp處理器)等處理器架構(gòu),然而在Qdsp中,如果訪問了非對齊的內(nèi)存,就會直接發(fā)生錯誤,直接把系統(tǒng)crush掉)那么在這種架構(gòu)下編程必須保證字節(jié)對齊.其他平臺可能沒有這種情況,但是最常見的是如果不按照適合其平臺要求對數(shù)據(jù)存放進(jìn)行對齊,會在存取效率上帶來損失。比如有些平臺每次讀都是從偶地址開始,如果一個int型(假設(shè)為32位系統(tǒng))如果存放在偶地址開始的地方,那?么一個讀周期就可以讀出這32bit,而如果存放在奇地址開始的地方,就需要2個讀周期,并對兩次讀出的結(jié)果的高低字節(jié)進(jìn)行拼湊才能得到該32bit數(shù)據(jù)。顯然在讀取效率上下降很多。?
一般的規(guī)則是,
1?對于基本數(shù)據(jù)類型對齊要求:
Char?類型一個字節(jié)對齊,可以從任何內(nèi)存地址讀取;
Short2個字節(jié),要求是從偶內(nèi)存地址讀取;
Int?4個字節(jié)(32位系統(tǒng)),要求是變量的內(nèi)存地址必須被4整除;
Long和int一樣(在32位系統(tǒng)中)
Double?8個字節(jié),要求是變量的內(nèi)存地址必須被8整除。
在c語言中存在struct和union結(jié)構(gòu)體類型,屬于復(fù)雜類型:
2??對于struct和union對齊要求是:
其成員中自身對齊值最大的那個值?。
注:結(jié)構(gòu)的總大小為結(jié)構(gòu)的字節(jié)邊界數(shù)(即該結(jié)構(gòu)中占用最大空間的變量的類型所占用的字節(jié)數(shù))的倍數(shù),對于結(jié)構(gòu)體最終大小,還要參考,指定對齊值n。
1)對于struct我們有:
s表示這個結(jié)構(gòu)體,X(i)表示其第i個成員(按順序從上往下)所占的大小;假使結(jié)構(gòu)體有m個成員;
結(jié)構(gòu)體的最終對齊值??
??其中基本類型對齊值:
??A(char)?=?1;
??A(short)?=?2;
??A(int)?=?4;
??A(long)?=?4;
??A(float)?=?4;
??A(double)?=?8;
?
????基本類型成員對齊值?A(x)?=?min(A(type),n?);????公式1
A(type)?為基本類型成員x?對應(yīng)的基本類型。
?????A(s)?=?min(max(A(1),A(2),...,A(m)),n)???????公式2
(這個一個遞歸函數(shù),主要原因是成員Xi有可能也是一個結(jié)構(gòu)體,嵌套類型)
?其中:
?由規(guī)則1可以得到公式?2的初始值
??struct結(jié)構(gòu)體最終占內(nèi)存大小Xs:
??定義H(x)表示前面x個成員最終占內(nèi)存大小;A(x)表示第x個成員的對齊值。可以有公式1給出,則:
If(H(x)%A(x+1)?==?0)
??H(x+1)=?H(x)+X(x+1);
Else
??H(x+1)=?H(x)+A(x+1)-H(x)%A(x+1)+X(x+1);?
?
其中?H(1)?=?X1,A(1)=?X1;??
????????????????????????????????????公式3
?
則:
If(H(m)%A(s)?==?0)
???Xs?=?H(m);
Else
???Xs?=?H(m)+A(s)-H(m)%A(s);?
?
????????????????????????????????????公式4?
2)對于union我們有:
?????U表示這個結(jié)構(gòu)體,Xi表示其第i個成員(按順序從上往下)所占的大小;假使結(jié)構(gòu)體有m個成員;
??????結(jié)構(gòu)體的最終對齊值??A(u)?=?min(max(A(1),A(2),...,A(m)),n)?公式5
Union結(jié)構(gòu)體最終占內(nèi)存大小Xu:
???定義H(x)表示前面x個成員最終占內(nèi)存大小;A(x)表示第x個成員的對齊值。可以有公式1給出,則:
??H(x+1)=?max(X(x+1),?H(x));
其中?H(1)?=?X1;
?
????????????????????????????????????公式6
???則:
If(H(m)%A(u)?==?0)
???Xu?=?H(m);
Else
???Xu?=?H(m)+A(u)-H(m)%A(u);?
?
????????????????????????????????????公式7?
?
3??指定對齊值:
?????如果我們想指定對齊值,可以在VC?IDE中,可以這樣修改:[Project]|[Settings],c/c++選項卡Category的Code?Generation選項的Struct?Member?Alignment中修改,默認(rèn)是8字節(jié),針對全部變量,如果想動態(tài)改變部分,在vc中可以用宏命令?#pragma?pack?(n)時的指定對齊值n,#pragma?pack()取消,之間的數(shù)據(jù)都是指定為n,但不一定為對齊n。?最終的數(shù)據(jù)成員對齊值為:?自身對齊值和指定對齊值中小的那個值?。(這個很重要,請好好理解)。
?
4??結(jié)構(gòu)中帶有結(jié)構(gòu)
????不必考慮整個子結(jié)構(gòu),只考慮子結(jié)構(gòu)的基本類型并參照前面的規(guī)則來分配空間。
?空結(jié)構(gòu)(即不帶任何的方法和數(shù)據(jù))占用的1字節(jié)的空間。
5??枚舉中(enum)?
???枚舉始終占用4字節(jié)的空間。
?
6??結(jié)構(gòu)中成員
????結(jié)構(gòu)的靜態(tài)成員不對結(jié)構(gòu)的大小產(chǎn)生影響,因為靜態(tài)變量的存儲位置與結(jié)構(gòu)的實例地址無關(guān)。??
要理解上面的對齊規(guī)則,最好是分析一些典型的對齊例子:
例子1
???struct?MyStruct?
{?
??char?dda;?
??double?dda1;?
??int?type?
};?
?
默認(rèn)指定對齊值n?=?8(vc),其他自己查看,則n?=?8;
由公式1?得到此結(jié)構(gòu)體的最終對齊值為?A(s)?=?8
有上面公式2?,3?可以得到?X(MyStruct)(=sizeof(MyStruct))?:
H(1)?=?1,?H(2)?=(H(1))?1+7+8?=?16;?H(3)?=(H(2))?16+4?=?20;
X(s)?=?(H(3))20?+?(A(s)-H(3)%A(s))?4?=?24;
所以sizeof(MyStruct)?=?24;
例子2
#pragma??pack(2)
?struct?MyStruct?
{?
??char?dda;???A(1)?=?1?
??double?dda1;?A(2)?=?2?
??int?type???A(3)?=?2?
};?
#pragma??pack()
由公式1,2得到此結(jié)構(gòu)體的最終對齊值為?A(s)?=?2
有上面公式3?,4?可以得到?X(MyStruct)(sizeof(MyStruct))?:
H(1)?=?1,?H(2)?=(H(1))?1+?(A(2)-H(1)%A(2))?1+(X2)8?=?10;?因為H(2)%A(3)==0;
所以?H(3)?=(H(2))?10+4?=?14;
因為H(3)%A(s)?==?0;
所以X(s)?=?(H(3))?14;
所以sizeof(MyStruct)?=?14;
例子3:
???這里有個結(jié)構(gòu)體嵌套例子,對于結(jié)構(gòu)體中的結(jié)構(gòu)體成員,不要認(rèn)為它的對齊方式就是他的大小,看下面的例子:
struct?s1
{
char?a[8];?
};
struct?s2
{
double?d;?
};
struct?s3
{
s1?s;?
char?a;?
};
struct?s4
{
s2?s;?
char?a;?
};
默認(rèn)指定對齊值n?=?8;
A(s1)?=?1;A(s2)?=min(min(A(double),?8),8)?=8?;?
A(s3)?=?min(max(A(x1)=min(A(s1)?=?1,8)?=?1,A(x2)?=?1),8)?=?1;
A(s3)?=?min(max(A(x1)=min(A(s2)?=?8,8)?=?8,A(x2)?=?1),8)?=?8;
X(s1)?=?8;
X(s2)?=?8;
X(s3)?=?9;
X(s4)?=?16;
具體過程自己可以分析一下;
?例子?4:
#pragma?pack(4)
union?u1
{
double?b;?A(1)??=?4
int?a;????A(2)??=?4
}x1;
#pragma?pack()
#pragma?pack(8)
union?u2
{
char?a[13];?A(1)?=?1
double?b;??A(2)?=?8
}x2;
#pragma?pack()
A(u1)??=?min(max(A(1),A(2))=4,n=4)?=?4;
A(u2)??=min(max(A(1),A(2))=8,n=8)?=?8;
對于u1,H(1)?=?8;?H(2)?=?max(H(1)8,X(2)4)?=?8;
因為H(2)%A(u1)?==?0;
所以?X(u1)?=?H(2)?=?8;
對于u2,H(1)?=?13,?H(2)=max(H(1),X(2)8)?=?13;
因為?H(2)%A(u2)?!=0;
所以?X(u2)?=?H(2)?+?A(u2)-H(2)%A(u2)=?13+3=16;
也上只是一些測試?yán)?#xff0c;真實的結(jié)構(gòu)體都比較龐大,一般用sizeof就可以了,但心里要清楚,每個成員的偏移量和填充的字節(jié),這些都可以由上面的公式推出來,我這里就暫時不推導(dǎo)了(但這個才是我們平時最應(yīng)該注意的)。
五?總結(jié)
?????對于字節(jié)的理解其實這些還不夠,掌握這些只是作為頂級c程序員最基本的要求(路還很長),有細(xì)節(jié)需要參考一下cpu的手冊。其實在實際編程當(dāng)中,出現(xiàn)字節(jié)對齊的原因是通信的要求,不管是通過tcp/ip(互聯(lián)網(wǎng)),這樣一般協(xié)議頭部都是一個字節(jié)對齊,這樣對方解釋的時候是只需按協(xié)議解析就正確了;或者是動態(tài)庫調(diào)用,給別人的接口函數(shù)對應(yīng)參數(shù),如果是沒有滿足字節(jié)對齊的要求(不相同),如果進(jìn)行強制類型轉(zhuǎn)換或者按偏移量訪問變量,就有可能出現(xiàn)錯誤(某些嵌入式cpu)或者意想不到問題,所以對于嵌入式開發(fā)的程序員最好是心里有數(shù)。
????最后請希望自己成為頂級c程序員的coders,來設(shè)計一個memcpy函數(shù),為什么要設(shè)計這個函數(shù)呢,如果你看過很多大工程的代碼,你就明白了,這個函數(shù)使用率相當(dāng)高,strcpy這個的優(yōu)化版本內(nèi)部都是調(diào)用memcpy來完成,這是系統(tǒng)函數(shù),每個平臺自己都實現(xiàn)了這個函數(shù),而且里面充滿奇淫巧計,呵呵,看看自己會長見識。
以下是高通平臺的memcpy函數(shù):
typedef??unsigned?int?uint_t;
typedef??unsigned?char?uchar_t;
void?*
memcpy(void?*?d,?const?void?*s,size_t?n)
{
???size_t?i;
???uint_t?align?=?sizeof(uint_t?)?-1;
???if(?((uint_t)d&align)|?((uint_t)s&align)|?(n&align))
??{
?????uchar_t??*dest?=?(uchar_t??*)d;
?????uchar_t??*src?=?(uchar_t??*)s;
?????for(i?=?0;?i?<?n;?++i)
?????{
????????*dest?++?=?*src++;
?????}
?}
?else
?{
?????uint_t??*dest?=?(uint_t?*)d;
?????uint_t??*src?=?(uint_t?*)d;
?????n?/=sizeof(uint_t?);
?????for(i?=?1;?i?<?n;?++i)
??????{
????????*dest?++?=?*src++;
??????}
?}
?return?d;
}
?
注:?以上只是我學(xué)習(xí)中文檔輸出,可能會有錯誤(什么類型的錯誤都可能發(fā)生),我希望讀者能幫我一起改正,希望大家一起努力,朝頂級c程序員前進(jìn),讀者有什么意見都可以在回復(fù)中提出,我會繼續(xù)的,就像我前一篇文章所說一樣,目的把我學(xué)到的東西分享給大家,希望大家好好學(xué)
?
總結(jié)
以上是生活随笔為你收集整理的顶级c程序员之路 选学篇-1 深入理解字节,字节序与字节对齐的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 《深入理解Kafka:核心设计与实践原理
- 下一篇: 顶级c程序员之路 基础篇 - 第一章 关