顶级C程序员之路
?hi,大家好,我是極客君,今天分享一篇幾年前在CSDN上發表的文章。C語言是我非常喜歡的語言,也是眾多高級語言的鼻祖。
C語言的優勢:
門檻低:核心特性和標準庫都比較簡單;
上限高:語法精簡,編程自由度高,可以玩出各種高端操作;
性能強:極少抽象, 幾乎完全對應到匯編實現,性能強,首選底層系統開發語言。
TIOBE編程社區揭曉了各大編程語言的排行情況:
Google搜索排行榜:
知乎熱門討論:
大量重量級軟件都是C寫的,比如Linux,UNIX,IOS內核,windows內核,Android內核,jvm虛擬機,CPython,Nginx,Redis,MySQL,GCC,GDB等等,可以看到,整個世界的基礎架構都是在 C 語言之上運行的,C語言是你必須學習的語言。因為這個世界上絕大多數編程語言都是 C-like 的語言,也是在不同的方面來解決 C 語言的各種問題。學習 C 語言是一個合格的程序員的必經之路。
下面章節進入今天的主題
深入理解字節,字節序與字節對齊?
? ? ??? ? ? ? ? ? ? ? ?
一 總述
??
作為一個職業的coder玩家,首先應該對計算機的字節有所了解。我們經常談到的2進制流,字節(字符)流,數據類型流(針對編程),結構流等說法,2進制流,0和1的操作,屬于cpu級,從字符流向上都是我們玩家關心,字節流屬于操作系統級,今天談的就是字節流操作。
二? 字節
???
因為計算機用二進制,所以希望基本存儲單位的是2的n次方(和硬件設計有關)。這樣讀取字節的時候,開銷不會太高,可以達到最大性能,因為剛開始,計算機是美國發明的,西文字符(英文字母大小寫,數字,其他特殊字符等將近有1百多個,所以用7位來表示,這樣可以把所有西文字符表達完,再加上一位校檢位,一共8位,由于ASCⅡ的廣泛應用,所有后來,一個字節占8位就成了國際規定的標準了,一直沿用至今(有待研究,但不是今天的主題)。
???
一個字節占8個2進制位,數據類型流,就是在c語言里面的數據類型占多少個字節。然后直接操作數據類型。在目前的32位系統中,c語言的基本數據類型有以下幾種:
Char??占一個字節?(-2^7?-?2^7-1?,-128?到?127)最高位?為符號位
Unsigned?char?占一個字節???(0-2^8,0到255)
Short???占2個字節?(-2^15?-?2^15-1?,?-32768到32767)最高位?為符號位
Unsigned?short?占2個字節?(0?-?2^16?,?0到65536)
Int?(字長,對于32位機為32位,16位機為16位,長度不固定,和系統平臺有關,處理器位數有關,代表尋址空間),在32位機占4個字節(-2^31-2^31?)最高位?為符號位
Unsigned??int?占4個字節(0-2^32?)
Long?int?為4個字節,在16,32位機都占4個字節(-2^31-2^31?)最高位?為符號位
Unsigned??long?int占4個字節(0-2^32?)
sizeof(short)?<=?sizeof(int)?<=?sizeof(long)??
在32位機 int和long都是32位,沒有什么大的區別,但還是有些小的區別,有時最好用long,他大小固定,如果到其他平臺,比如64位,他還是占4個字節,而int卻占8個字節,可以增加代碼的可移植性。
Long?long?int?占8個字節(c99標準)??
Unsigned?long?long?int?占8個字節(c99標準)??
浮點類型?由于浮點類型和整形的編碼不一樣,所以浮點型需要特殊分析。
Float?占4個字節 ?
Double?占8個字節?
三 字節序
為什么有字節序這個概念存在呢?
不同的CPU有不同的字節序類型,這些字節序是指整數在內存中保存的順序?這個叫做主機序 ,就是多個字節在內存中擺放位置順序和解釋順序。
最常見的有兩種?:
1. Little endian:將低序字節存儲在起始地址? 4321
2. Big endian:將高序字節存儲在起始地址??? 1234
LE?little-endian?
最符合人的思維的字節序,地址低位存儲值的低位 ,地址高位存儲值的高位,怎么講是最符合人的思維的字節序,是因為從人的第一觀感來說低位值小,就應該放在內存地址小的地方,也即內存地址低位,反之,高位值就應該放在內存地址大的地方,也即內存地址高位。
BE?big-endian?
最直觀的字節序,地址低位存儲值的高位 ,地址高位存儲值的低位,為什么說直觀,不要考慮對應關系,只需要把內存地址從左到右按照由低到高的順序寫出,把值按照通常的高位到低位的順序寫出兩者對照,一個字節一個字節的填充進去 。
例子:如果我們將0x1234abcd寫入到以0x0000開始的內存中,則結果為
內存地址? ? ? ? big-endian???little-endian
0x0000? ? ? ????0x12?????? ? ? ? ??0xcd
0x0001? ? ? ? ? 0x34? ? ? ? ? ? ? ?0xab
0x0002? ? ? ? ??0xab? ? ? ? ? ? ?? 0x34
0x0003? ? ? ? ? 0xcd? ? ? ? ? ? ? ?0x12
網絡字節序
網絡字節序是TCP/IP協議棧中規定好的一種數據表示格式,它與具體的CPU類型、操作系統等無關,從而可以保證數據在不同主機之間傳輸時能夠被正確解釋。網絡字節順序采用big endian排序方式。
為了進行轉換 bsd socket提供了轉換的函數?有下面四個:
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類型從主機序轉換到網絡序
htonl?把unsigned?long類型從主機序轉換到網絡序
ntohs?把unsigned?short類型從網絡序轉換到主機序
ntohl?把unsigned?long類型從網絡序轉換到主機序
在使用little endian的系統中這些函數會把字節序進行轉換, 在使用big endian類型的系統中這些函數會定義成空,什么都不做。
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)\
))
一般c語言編寫程序的字節序都是系統相關的,叫主機序,即指系統處理器本身所采用的字節序,java的字節碼是big-endian 和網絡字節序一樣,所以他和網絡通信不需要關心字節序問題,如果要和其他平臺進行通信,都要進行字節序轉換,一般采用標準化的網絡序進行傳輸:
發送端:主機序->網絡序
接收端:?網絡序->主機序
不管哪種字節序,目的都是大家對01二進制串解釋都一樣,?不然兩方的解釋不一樣就可能會產生嚴重的問題。
對于IP頭定義里面ihl和version字段需要考慮主機字節序;__be 表示big endian
網絡上流傳一個測試自己系統是什么字節序函數代碼:
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;
?? ? ?}?
}?
注:不同的CPU上運行不同的操作系統,字節序也是不同的,參見下表:
處理器? | ? 操作系統 | ?字節序 |
Alpha? ? ? ? ? ?? | 全部 | ?Little?endian |
ARM | 全部 | ?Little?endian |
Intelx86 | 全部? ? ? | ?Little?endian? |
AMD | 全部? ? ? | ?Little?endian |
MIPS? ? ? ? ? ? ?? | NT? ? | ?Little?endian |
MIPS? ? ? ? ? ? ? | UNIX? ? ? | ?Big?endian? |
PowerPC? ? ? ? ? ? | NT? ? ?? | ?Little?endian |
PowerPC? ? ? ? ?? | 非NT??? ? | ?Big?endian? |
x86,AMD,ARM等芯片平臺是小端字節序系統
PowerPC?,PPC等芯片平臺是大端字節序系統
為什么不統一字節序?
1. 計算都是從低位開始的,因此計算機內部處理采用小端序,效率較高。
2. 對于大端序,由于符號位在高位,因此對于數據正負或大小的判斷也就方便許多;其次,大端序也更符合人類的閱讀習慣。
3. 由于大小端各有優劣,各個芯片廠商的堅持自己設計,字節序的問題也就一直沒有統一。
字節序總結
不同處理器之間采用的字節序可能不同。
有些處理器的字節序是確定的,有些處理器的字節序是可配置的。
網絡序一般統一為大端序。
數據從本地傳輸到網絡,需要轉換為網絡序,接收到的網絡數據需要轉換為本地序后使用。
C提供了一組接口用于整型數據在本地序和網絡序之間的轉換。
多字節數據對象才需要轉字節序,例如int,short等,而char不需要。
由于處理器是按照IEEE標準處理float和double的,因此也不需要轉字節序。
由于Java虛擬機的存在,Java不需要考慮大小端的問題。
四 字節對齊
首先我看哈程序的優化種類:
Cpu級優化((讀內存),流水線,cache,現在多核等)->2進制級(即01代碼)優化(現在估計沒有人去做了)->匯編級(指令)優化->高級程序里面的代碼級優化(位運算,前++和后++,數組和指針,if?else和switch?case等優化)->算法優化(流程優化)->軟件架構級優化......
而現在我們討論的字節對齊屬于cpu級優化,可以加速cpu讀取內存時間。
什么是字節對齊?
現代計算機中內存空間都是按照byte劃分的,從理論上講似乎對任何類型的變量的訪問可以從任何地址開始,但實際情況是在訪問特定類型變量的時候經常在特定的內存地址訪問,這就需要各種類型數據按照一定的規則在空間上排列,而不是順序的一個接一個的排放,這就是對齊。?
為什么要字節對齊
原因一: CPU讀取內存效率
各個硬件平臺對存儲空間的處理上有很大的不同。一些平臺對某些特定類型的數據只能從某些特定地址開始存取。
比如有些架構的CPU在訪問一個沒有進行對齊的變量的時候會發生錯誤(比如高通平臺,一般的手機平臺都采用美國高通公司開發平臺,對于無線上網卡來說,現在都是多核,要么是arm9+arm11,要么是arm9+2個Qdsp(Q是表示高通的dsp處理器)等處理器架構,然而在Qdsp中,如果訪問了非對齊的內存,就會直接發生錯誤,直接把系統crush掉)那么在這種架構下編程必須保證字節對齊。
其他平臺可能沒有這種情況,但是最常見的是如果不按照適合其平臺要求對數據存放進行對齊,會在存取效率上帶來損失。比如有些平臺每次讀都是從偶地址開始,如果一個int型(假設為32位系統)如果存放在偶地址開始的地方,那么一個讀周期就可以讀出這32bit。
而如果存放在奇地址開始的地方,就需要2個讀周期,并對兩次讀出的結果的高低字節進行拼湊才能得到該32bit數據。顯然在讀取效率上下降很多。?
原因二 : Cache親和性
結構的數據跨越兩個cache line,就意味著兩次load或者兩次store。如果數據結構是cache line對齊的,? 就有可能減少一次讀寫。
掌握字節對齊的估算方法
學會估算結構大小,可以幫助我們更好地設計程序的數據結構,比如合理地節約內存,提供數據訪問效率等。
1. 對于基本數據類型對齊要求:
Char 類型一個字節對齊,可以從任何內存地址讀取;
Short2個字節,要求是從偶內存地址讀取;
Int 4個字節(32位系統),要求是變量的內存地址必須被4整除;
Long和int一樣(在32位系統中)
Double 8個字節,要求是變量的內存地址必須被8整除。
2. 對于struct和union對齊要求是:
在c語言中存在struct和union結構體類型,屬于復雜類型,成員中自身對齊值最大的那個值?。
注:結構的總大小為結構的字節邊界數(即該結構中占用最大空間的變量的類型所占用的字節數)的倍數, 對于結構體最終大小,還要參考,指定對齊值n。
以下部分屬于具體計算方法(有很多公式,但這些公式你可能從來沒有看到過),不想看可以跳過這一節。
struct
s表示結構體,假使結構體有m個成員,?定義A(x)表示第x個成員的對齊值, X(i)表示其第i個成員(按順序從上往下)所占的大小,?H(x)表示前面x個成員最終占內存大小, 則結構體的大小可以通過下面公式估算出來:
初始值--基本類型type對齊值:
??A(char)?=?1;
??A(short)?=?2;
??A(int)?=?4;
??A(long)?=?4;
??A(float)?=?4;
??A(double)?=?8;
基本類型成員對齊值,type為基本類型成員x對應的基本類型:
第x個成員對齊值:
A(x) = min(A(type),n );????公式1
整個結構對齊值:
A(s)?= min(max(A(1),A(2),...,A(m)),n)????公式2
則struct結構體前x+1個成員和前x成員之間計算公式:
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)?= X(1),A(1)=?X(1);???公式3
則結構體最終結構體大小X(s)為:
If(H(m)%A(s)?==?0)
???X(s) =?H(m);
Else
???X(s) =?H(m)+A(s)-H(m)%A(s);??公式4?
union
U表示這個結構體,X(i)表示其第i個成員(按順序從上往下)所占的大小;假使結構體有m個成員;結構體的最終對齊值??
A(u)?=?min(max(A(1),A(2),...,A(m)),n)?公式5
定義H(x)表示前面x個成員實際占內存大小;A(x)表示第x個成員的對齊值,可以有公式1給出,?Union結構體最終占內存大小為X(u),?則:
則union結構體前x+1個成員和前x成員之間計算公式:
H(x+1)=?max(X(x+1),?H(x));
其中?H(1)?=?X(1);???公式6
則最終union結構體大小X(u)為:
If(H(m)%A(u)?==?0)
???X(u) =?H(m);
Else
???X(u)?=?H(m)+A(u)-H(m)%A(u);??公式7?
公式4和公式7一般實現:
指定對齊值
VC/VS編譯器
如果我們想指定對齊值,可以在VC IDE中,可以這樣修改:[Project]|[Settings],c/c++選項卡Category的Code Generation選項的Struct Member Alignment中修改,默認是8字節,針對全部變量,如果想動態改變部分,在vc中可以用宏命令?
#pragma pack (n)時的指定對齊值n
#pragma pack()取消,之間的數據都是指定為n,但不一定為對齊n。
最終的數據成員對齊值為:?自身對齊值和指定對齊值中小的那個值。
GCC編譯器
__attribute__((aligned(n)))?表示所定義的變量為n字節對齊。
__attribute__((__packed__))? 表示結構體內不填充多余的字節,一般用于通信協議結構體定義;
__attribute__((__aligned__(SMP_CACHE_BYTES)))表示結構按cache對齊,提高數據訪問效率。
結構中帶有結構
不必考慮整個子結構,只考慮子結構的基本類型并參照前面的規則來分配空間。空結構(即不帶任何的方法和數據)占用的1字節的空間。
枚舉中(enum)?
枚舉始終占用4字節的空間。
結構中成員
結構的靜態成員不對結構的大小產生影響,因為靜態變量的存儲位置與結構的實例地址無關,要理解上面的對齊規則,最好是分析一些典型的對齊例子:
例子1:
struct?MyStruct?{?
??char?dda;?
??double?dda1;?
??int?type?
};?
默認指定對齊值n = 8(vc),其他自己查看,則n = 8;
由公式1?得到此結構體的最終對齊值為?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得到此結構體的最終對齊值為?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:
這里有個結構體嵌套例子,對于結構體中的結構體成員,不要認為它的對齊方式就是他的大小,看下面的例子:
struct?s1{
char?a[8];?
};
struct?s2{
double?d;?
};
struct?s3{
s1?s;?
char?a;?
};
struct?s4{
s2?s;?
char?a;?
};
默認指定對齊值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(s4)?=?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;
以上只是一些測試例子,真實的結構體都比較龐大,一般用sizeof就可以了,但心里要清楚,每個成員的偏移量和填充的字節,這些都可以由上面的公式推出來(平時最應該注意的),我這里就暫時不推導了。
字節對齊利弊
1. 對齊意味著可能有內存浪費(特別是數組這樣連續分配的數據結構),所以需要在空間和時間兩方面權衡。
2. 跨平臺通信場景,每個平臺程序字節對齊不一樣,可能會導致內存空間解析出錯,所以一般采用1字節對齊,中間不含填充數據,但這樣會導致一些屬性訪問性能下降,需要綜合考慮。
字節對齊總結
結構體成員合理安排位置,節省空間,提高性能
跨平臺數據結構可考慮1字節對齊,節省空間,解析安全,影響訪問效率
跨平臺數據結構進行結構優化(對齊填充),提高訪問效率,解析風險,不節省空間
本地數據采用chace對齊,提高訪問效率
32位與64位默認對齊數不一樣
五 最后總結
?
對于字節的理解,其實這些還不夠,掌握這些只是作為頂級c程序員最基本的要求(路還很長),細節需要參考一下cpu的手冊。
其實在實際編程當中,出現字節對齊的原因是通信的要求,如果是通過tcp/ip(互聯網)通信,這樣一般協議頭部都是一個字節對齊,這樣對方解釋的時候是只需按協議解析就正確了;
或者是動態庫調用,給別人的接口函數對應參數,如果是沒有滿足字節對齊的要求(不相同),如果進行強制類型轉換或者按偏移量訪問變量,就有可能出現錯誤(某些嵌入式cpu)或者意想不到問題,所以對于嵌入式開發的程序員最好是心里有數。
??
最后,讓我們來設計一個memcpy函數,為什么要設計這個函數呢,如果你看過很多大型工程代碼,你就明白了,這個函數使用率相當高,strcpy這個的優化版本內部都是調用memcpy來完成,這是系統函數,每個平臺自己都實現了這個函數,而且里面充滿很多編程技巧,看看自己會長見識。
參考:
高通芯片平臺的memcpy函數:
技術:?內存對齊? ? 循環展開 (局部性原理)? 批量copy
glibc庫函數:
技術:?內存對齊? 批量copy ?
DPDK庫函數:
技術:?指令預取? 局部性原理? ?向量指令? cache對齊
注:?本文是我早期發表在CSDN上的文章,比較粗糙,現用公眾號記錄一下,希望和大家一起努力,朝頂級C程序員前進
推薦閱讀
C++內存管理全景指南
C++的最后一道坎|百萬年薪的程序員
Linux調度系統全景指南(終結篇-調度優化)
總結
- 上一篇: 硬核分析|腾讯云原生OS内存回收导致关键
- 下一篇: 顶级极客技术挑战赛,你敢来挑战吗?| 大