面試???,項目易錯,長文詳解C/C++中的字節(jié)對齊
引入主題,看代碼
我們先來看看以下程序
//編譯器:https://tool.lu/coderunner/
//來源:技術(shù)讓夢想更偉大
//作者:李肖遙
#include?
using?namespace?std;
struct?st1?
{
?char?a?;
?int??b?;
?short?c?;
};
struct?st2
{
??short?c?;
??char??a?;
??int???b?;
};
int?main()
{
?cout<<"sizeof(st1)?->?"<?cout<<"sizeof(st2)?->?"<?
?return?0?;
}
編譯的結(jié)果如下:
問題來了,兩個結(jié)構(gòu)體的內(nèi)容一樣,只是換了個位置,為什么sizeof(st)
的時候大小不一樣呢?
沒錯,這正是因為內(nèi)存對齊的影響,導(dǎo)致的結(jié)果不同。對于我們大部分程序員來說,都不知道內(nèi)存是怎么分布的。
實(shí)際上因為這是編譯器該干的活,編譯器把程序中的每個數(shù)據(jù)單元安排在合適的位置上,導(dǎo)致了相同的變量,不同聲明順序的結(jié)構(gòu)體大小的不同。
幾種類型數(shù)據(jù)所占字節(jié)數(shù)
int,long int,short int的寬度和機(jī)器字長及編譯器有關(guān),但一般都有以下規(guī)則(ANSI/ISO制訂的)
-
sizeof(short int)
<=sizeof(int)
-
sizeof(int)
<=sizeof(long int)
-
short int
至少應(yīng)為16位(2字節(jié)) -
long int
至少應(yīng)為32位
數(shù)據(jù)類型 | 16位編譯器 | 32位編譯器 | 64位編譯器 |
---|---|---|---|
char | 1字節(jié) | 1字節(jié) | 1字節(jié) |
char* | 2字節(jié) | 4字節(jié) | 8字節(jié) |
short int | 2字節(jié) | 2字節(jié) | 2字節(jié) |
int | 2字節(jié) | 4字節(jié) | 4字節(jié) |
unsigned int | 2字節(jié) | 4字節(jié) | 4字節(jié) |
float | 4字節(jié) | 4字節(jié) | 4字節(jié) |
double | 8字節(jié) | 8字節(jié) | 8字節(jié) |
long | 4字節(jié) | 4字節(jié) | 8字節(jié) |
long long | 8字節(jié) | 8字節(jié) | 8字節(jié) |
unsigned long | 4字節(jié) | 4字節(jié) | 8字節(jié) |
什么是對齊
現(xiàn)代計算機(jī)中內(nèi)存空間都是按照byte劃分的,從理論上講似乎對任何類型的變量的訪問都可以從任何地址開始,但實(shí)際情況是在訪問特定變量的時候經(jīng)常在特定的內(nèi)存地址訪問。
所以這就需要各類型數(shù)據(jù)按照一定的規(guī)則在空間上排列,而不是順序的一個接一個的排放,這就是對齊。內(nèi)存對齊又分為自然對齊和規(guī)則對齊。
對于內(nèi)存對齊問題,主要存在于struct和union等復(fù)合結(jié)構(gòu)在內(nèi)存中的分布情況,許多實(shí)際的計算機(jī)系統(tǒng)對基本類型數(shù)據(jù)在內(nèi)存中存放的位置有限制,它們要求這些數(shù)據(jù)的首地址的值是某個數(shù)M(通常是4或8);
對于內(nèi)存對齊,主要是為了提高程序的性能,數(shù)據(jù)結(jié)構(gòu),特別是棧,應(yīng)盡可能在自然邊界上對齊,經(jīng)過對齊后,cpu的內(nèi)存訪問速度大大提升。
自然對齊
指的是將對應(yīng)變量類型存入對應(yīng)地址值的內(nèi)存空間,即數(shù)據(jù)要根據(jù)其數(shù)據(jù)類型存放到以其數(shù)據(jù)類型為倍數(shù)的地址處。
例如char類型占1個字節(jié)空間,1的倍數(shù)是所有數(shù),因此可以放置在任何允許地址處,而int類型占4個字節(jié)空間,以4為倍數(shù)的地址就有0,4,8等。編譯器會優(yōu)先按照自然對齊進(jìn)行數(shù)據(jù)地址分配。
規(guī)則對齊
以結(jié)構(gòu)體為例就是在自然對齊后,編譯器將對自然對齊產(chǎn)生的空隙內(nèi)存填充無效數(shù)據(jù),且填充后結(jié)構(gòu)體占內(nèi)存空間為結(jié)構(gòu)體內(nèi)占內(nèi)存空間最大的數(shù)據(jù)類型成員變量的整數(shù)倍。
實(shí)驗對比
首先看這個結(jié)構(gòu)體
typedef?struct?test_32
{
?char?a;
?short?b;
?short?c;
?char?d;
}test_32;
首先按照自然對齊,得到如下圖的內(nèi)存分布位置,第一個格子地址為0,后面遞增。
編譯器將對空白處進(jìn)行無效數(shù)據(jù)填充,最后將得到此結(jié)構(gòu)體占內(nèi)存空間為8字節(jié),這個數(shù)值也是最大的數(shù)據(jù)類型short的2個字節(jié)的整數(shù)倍。
如果稍微調(diào)換一下位置的結(jié)構(gòu)體
typedef?struct?test_32
{
?char?a;
?char?b;
?short?c;
?short?d;
}test_32;
同樣按照自然對齊如下圖分布
可以看到按照自然對齊,變量之間沒有出現(xiàn)間隙,所以規(guī)則對齊也不用進(jìn)行填充,而這里有顏色的方格有6個,也就是6個字節(jié)
按照規(guī)則對齊,6字節(jié)是此結(jié)構(gòu)體中最大數(shù)據(jù)類型short的整數(shù)倍,因此此結(jié)構(gòu)體為6字節(jié),后面的空白不需理會,可以實(shí)際編譯一下運(yùn)行,結(jié)果和分析一致為6個字節(jié)。
double的情況
我們知道32位處理器一次只能處理32位也就是4個字節(jié)的數(shù)據(jù),而double是8字節(jié)數(shù)據(jù)類型,這要怎么處理呢?
如果是64位處理器,8字節(jié)數(shù)據(jù)可以一次處理完畢,而在32位處理器下,為了也能處理double8字節(jié)數(shù)據(jù),在處理的時候?qū)?code style="font-size: 14px;overflow-wrap: break-word;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;background-color: rgba(27, 31, 35, 0.05);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(40, 202, 113);">會把double拆分成兩個4字節(jié)數(shù)進(jìn)行處理,從這里就會出現(xiàn)一種情況如下:
typedef?struct?test_32
{
?char?a;
?char?b;
?double?c;
}test_32;?
這個結(jié)構(gòu)體在32位下所占內(nèi)存空間為12字節(jié),只能拆分成兩個4字節(jié)進(jìn)行處理,所以這里規(guī)則對齊將判定該結(jié)構(gòu)體最大數(shù)據(jù)類型長度為4字節(jié),因此總長度為4字節(jié)的整數(shù)倍,也就是12字節(jié)。
這個結(jié)構(gòu)體在64位環(huán)境下所占內(nèi)存空間為16字節(jié),而64位判定最大為8字節(jié),所以結(jié)果也是8字節(jié)的整數(shù)倍:16字節(jié)。這里的結(jié)構(gòu)體中的double沒有按照自然對齊放置到理論上的8字節(jié)倍數(shù)地址處,我認(rèn)為這里編譯器也有根據(jù)規(guī)則對齊做出相應(yīng)的優(yōu)化,節(jié)省了4個多余字節(jié)。
這部分各位可以按照上述規(guī)則自行分析測試。
數(shù)組
對齊值為:min(數(shù)組元素類型,指定對齊長度)
。但數(shù)組中的元素是連續(xù)存放,存放時還是按照數(shù)組實(shí)際的長度。
如char t[9],對齊長度為1,實(shí)際占用連續(xù)的9byte。然后根據(jù)下一個元素的對齊長度決定在下一個元素之前填補(bǔ)多少byte。
嵌套的結(jié)構(gòu)體
假設(shè)
struct?A
{
??......
??struct?B?b;
??......
};
對于B結(jié)構(gòu)體在A中的對齊長度為:min(B結(jié)構(gòu)體的對齊長度,指定的對齊長度)
。
B結(jié)構(gòu)體的對齊長度為:上述2中結(jié)構(gòu)整體對齊規(guī)則中的對齊長度。舉個例子
//編譯器:https://tool.lu/coderunner/
//來源:技術(shù)讓夢想更偉大
//作者:李肖遙
#include?
#include?
using?namespace?std;
#pragma?pack(8)
struct?Args
{
?char?ch;
?double?d;
?short?st;
?char?rs[9];
?int?i;
}?args;
struct?Argsa
{
??char?ch;
??Args?test;
??char?jd[10];
??int?i;
}arga;
int?main()
{
?cout<<"Args:"<?cout<<""<<(unsigned?long)&args.i-(unsigned?long)&args.rs<?cout<<"Argsa:"<?cout<<"Argsa:"<<(unsigned?long)&arga.i?-(unsigned?long)&arga.jd<?cout<<"Argsa:"<<(unsigned?long)&arga.jd-(unsigned?long)&arga.test<?return?0;
}
輸出結(jié)果:
改成#pragma pack (16)結(jié)果一樣,這個例子證明了三點(diǎn):
-
對齊長度長于struct中的類型長度最長的值時,設(shè)置的對齊長度等于無用
-
數(shù)組對齊的長度是按照數(shù)組成員類型長度來比對的
-
嵌套的結(jié)構(gòu)體中,所包含的結(jié)構(gòu)體的對齊長度是結(jié)構(gòu)體的對齊長度
指針
主要是因為32位和64位機(jī)尋址上,來看看例子
//編譯器:https://tool.lu/coderunner/
//來源:技術(shù)讓夢想更偉大
//作者:李肖遙
#include?
#include?
using?namespace?std;
#pragma?pack(4)
struct?Args
{
?int?i;
?double?d;
?char?*p;?
?char?ch;?
?int?*pi;
}args;
int?main()
{????
?cout<<"args?length:"<?cout<<"args1:"<<(unsigned?long)&args.ch-(unsigned?long)&args.p<?cout<<"args2:"<<(unsigned?long)&args.pi-(unsigned?long)&args.ch<?return?0;
}
結(jié)果如下
pack | 4 | 8 |
---|---|---|
length | 32 | 40 |
args1 | 8 | 8 |
args2 | 4 | 8 |
內(nèi)存對齊的規(guī)則
-
數(shù)據(jù)成員對齊規(guī)則
結(jié)構(gòu)或聯(lián)合的數(shù)據(jù)成員,第一個數(shù)據(jù)成員放在offset為0的地方,以后每個數(shù)據(jù)成員的對齊按照#pragma pack
指定的數(shù)值和這個數(shù)據(jù)成員自身長度中,比較小的那個進(jìn)行。
例如struct a里存有struct b,b里有char,int ,double等元素,那b應(yīng)該從8的整數(shù)倍開始存儲。
-
結(jié)構(gòu)體作為成員
如果一個結(jié)構(gòu)里有某些結(jié)構(gòu)體成員,則結(jié)構(gòu)體成員要從其內(nèi)部"最寬基本類型成員"的整數(shù)倍地址開始存儲。
在數(shù)據(jù)成員完成各自對齊之后,結(jié)構(gòu)或聯(lián)合本身也要進(jìn)行對齊,對齊將按照#pragma pack指定的數(shù)值和結(jié)構(gòu)或聯(lián)合最大數(shù)據(jù)成員長度中,比較小的那個進(jìn)行。
-
1&2的情況下注意
當(dāng)#pragma pack的n值等于或超過所有數(shù)據(jù)成員長度的時候,這個n值的大小將不產(chǎn)生任何效果。
#pragma pack()用法詳解
-
作用
指定結(jié)構(gòu)體、聯(lián)合以及類成員的packing alignment;
-
語法
#pragma pack( [show] | [push | pop] [, identifier], n )
-
說明
-
pack提供數(shù)據(jù)聲明級別的控制,對定義不起作用;
-
調(diào)用pack時不指定參數(shù),n將被設(shè)成默認(rèn)值;
-
一旦改變數(shù)據(jù)類型的alignment,直接效果就是占用memory的減少,但是performance會下降;
-
語法具體分析
-
show:可選參數(shù)
顯示當(dāng)前packing aligment的字節(jié)數(shù),以warning message的形式被顯示;
-
push:可選參數(shù)
將當(dāng)前指定的packing alignment數(shù)值進(jìn)行壓棧操作,這里的棧是the internal compiler stack,同時設(shè)置當(dāng)前的packing alignment為n;如果n沒有指定,則將當(dāng)前的packing alignment數(shù)值壓棧;
-
pop:可選參數(shù)
從internal compiler stack中刪除最頂端的record;如果沒有指定n,則當(dāng)前棧頂record即為新的packing alignment數(shù)值;如果指定了n,則n將成為新的packing aligment數(shù)值;如果指定了identifier,則internal compiler stack中的record都將被pop直到identifier被找到,然后pop出identitier,同時設(shè)置packing alignment數(shù)值為當(dāng)前棧頂?shù)膔ecord;如果指定的identifier并不存在于internal compiler stack,則pop操作被忽略;
-
identifier:可選參數(shù)
當(dāng)同push一起使用時,賦予當(dāng)前被壓入棧中的record一個名稱;當(dāng)同pop一起使用時,從internal compiler stack中pop出所有的record直到identifier被pop出,如果identifier沒有被找到,則忽略pop操作;
-
n:可選參數(shù)
指定packing的數(shù)值,以字節(jié)為單位;缺省數(shù)值是8,合法的數(shù)值分別是1、2、4、8、16
例子
#include
#include
using?namespace?std;
?
#pragma?pack(4)
struct?m???
{
?int?a;??
?short?b;
?int?c;
};
int?main()
{
?cout?<<"結(jié)構(gòu)體m的大小:"<?cout?<??
??//?獲得成員a相對于m儲存地址的偏移量
?int?offset_b?=?offsetof(struct?m,?a);
?cout?<<"a相對于m儲存地址的偏移量:"<?system("pause");
?return?0;
}
從運(yùn)行結(jié)果來看我們可以證實(shí)上面內(nèi)存對齊規(guī)則的第一條:第一個數(shù)據(jù)成員放在offset為0的地方。
現(xiàn)在咱來看看上面結(jié)構(gòu)體是如何內(nèi)存對齊的;先用代碼打印它們每個數(shù)據(jù)成員的存儲地址的偏移量
//編譯器:https://tool.lu/coderunner/
//來源:技術(shù)讓夢想更偉大
//作者:李肖遙
#include
#include
using?namespace?std;
?
#pragma?pack(4)
struct?m???
{
?int?a;??
?short?b;
?int?c;
};
int?main()
{
?cout?<<"結(jié)構(gòu)體m的大小:"<?cout?<?int?offset_b?=?offsetof(struct?m,?a);//?獲得成員a相對于m儲存地址的偏移量
?int?offset_b1?=?offsetof(struct?m,?b);//?獲得成員a相對于m儲存地址的偏移量
?int?offset_b2?=?offsetof(struct?m,?c);//?獲得成員a相對于m儲存地址的偏移量
?
?cout?<<"a相對于m儲存地址的偏移量:"<?cout?<"b相對于m儲存地址的偏移量:"?<?cout?<"c相對于m儲存地址的偏移量:"?<?
?//system("pause");
?return?0;
}
在此c在結(jié)構(gòu)體中偏移量為8加上它自身(int)4個字節(jié),剛好是12(c的開始位置為8,所以要加它的4個字節(jié))
上面內(nèi)存結(jié)束為11,因為0-11,12是最大對齊數(shù)的整數(shù)倍,故取其臨近的倍數(shù),所以就取4的整數(shù)倍即12;
上圖中我用連續(xù)的數(shù)組來模仿內(nèi)存,如圖是它們的內(nèi)存對齊圖;
如果將最大內(nèi)存對齊數(shù)改為8,他將驗證內(nèi)存對齊規(guī)則中的第3條。
如果將其改為2,會發(fā)生什么:我們來看看:
//編譯器:https://tool.lu/coderunner/
//來源:技術(shù)讓夢想更偉大
//作者:李肖遙
#include
#include
using?namespace?std;
?
#pragma?pack(2)
struct?m???
{
?int?a;??
?short?b;
?int?c;
};
int?main()
{
?cout?<<"結(jié)構(gòu)體m的大小:"<?cout?<?int?offset_b?=?offsetof(struct?m,?a);//?獲得成員a相對于m儲存地址的偏移量
?int?offset_b1?=?offsetof(struct?m,?b);//?獲得成員a相對于m儲存地址的偏移量
?int?offset_b2?=?offsetof(struct?m,?c);//?獲得成員a相對于m儲存地址的偏移量
?
?cout?<<"a相對于m儲存地址的偏移量:"<?cout?<"b相對于m儲存地址的偏移量:"?<?cout?<"c相對于m儲存地址的偏移量:"?<?
?//system("pause");
?return?0;
}
對于這個結(jié)果,我們按剛才第一個例子我所分析的過程來分析這段代碼,得到的是10;
故當(dāng)我們將#pragma pack的n值小于所有數(shù)據(jù)成員長度的時候,結(jié)果將改變。
對齊的作用和原因
各個硬件平臺對存儲空間的處理上有很大的不同。如果不按照適合其平臺要求對數(shù)據(jù)存放進(jìn)行對齊,可能會在存取效率上帶來損失。
比如有些平臺每次讀都是從偶地址開始,如果一個int型在32位地址存放在偶地址開始的地方,那么一個讀周期就可以讀出;
而如果存放在奇地址開始的地方,就可能會需要2個讀周期,并對兩次讀出的結(jié)果的高低字節(jié)進(jìn)行拼湊才能得到該int數(shù)據(jù)。那么在讀取效率上下降很多,這也是空間和時間的博弈。
CPU每次從內(nèi)存中取出數(shù)據(jù)或者指令時,并非想象中的一個一個字節(jié)取出拼接的,而是根據(jù)自己的字長,也就是CPU一次能夠處理的數(shù)據(jù)長度取出內(nèi)存塊??傊?,CPU會以它“最舒服的”數(shù)據(jù)長度來讀取內(nèi)存數(shù)據(jù)
舉個例子
如果有一個4字節(jié)長度的指令準(zhǔn)備被讀取進(jìn)CPU處理,就會有兩種情況出現(xiàn):
-
4個字節(jié)起始地址剛好就在CPU讀取的地址處,這種情況下,CPU可以一次就把這個指令讀出,并執(zhí)行,內(nèi)存情況如下
-
而當(dāng)4個字節(jié)按照如下圖所示分布時
假設(shè)CPU還在同一個地址取數(shù)據(jù),則取到第一個4字節(jié)單元得到了1、2字節(jié)的數(shù)據(jù),但是這個數(shù)據(jù)不符合需要的數(shù)啊,所以CPU就要在后續(xù)的內(nèi)存中繼續(xù)取值,這才取到后面的4字節(jié)單元得到3、4字節(jié)數(shù)據(jù),從而和前面取到的1、2字節(jié)拼接成一個完整數(shù)據(jù)。
而本次操作進(jìn)行了兩次內(nèi)存讀取,考慮到CPU做大量的數(shù)據(jù)運(yùn)算和操作,如果遇到這種情況很多的話,將會嚴(yán)重影響CPU的處理速度。
因此,系統(tǒng)需要進(jìn)行內(nèi)存對齊,而這項任務(wù)就交給編譯器進(jìn)行相應(yīng)的地址分配和優(yōu)化,編譯器會根據(jù)提供參數(shù)或者目標(biāo)環(huán)境進(jìn)行相應(yīng)的內(nèi)存對齊。
什么時候需要進(jìn)行內(nèi)存對齊.
一般情況下都不需要對編譯器進(jìn)行的內(nèi)存對齊規(guī)則進(jìn)行修改,因為這樣會降低程序的性能,除非在以下兩種情況下:
-
這個結(jié)構(gòu)需要直接被寫入文件
-
這個結(jié)構(gòu)需通過網(wǎng)絡(luò)傳給其他程序
對齊的實(shí)現(xiàn)
可以通知給編譯器傳遞預(yù)編譯指令,從而改變對指定數(shù)據(jù)的對齊方法。
unsigned?int?calc_align(unsigned?int?n,unsigned?align)??
{??
????if?(?n?/?align?*?align?==?n)????????????
????????return?n;??
????return??(n?/?align?+?1)?*?align;??
}?
不過這種算法的效率很低,下面介紹一種高效率的數(shù)據(jù)對齊算法:
unsigned?int?calc_align(unsigned?int?n,unsigned?align)??
{??????
????return?((n?+?align?-?1)?&?(~(align?-?1)));??
}??
這種算法的原理是:
(align-1)
?:對齊所需的對齊位,如:2字節(jié)對齊為1,4字節(jié)為11,8字節(jié)為111,16字節(jié)為1111...
(&~(align-1))
?:將對齊位數(shù)據(jù)置位為0,其位為1
(n+(align-1)) & ~(align-1)
:對齊后的數(shù)據(jù)
總結(jié)
通常,我們寫程序的時候,不需要考慮對齊問題,編譯器會替我們選擇目標(biāo)平臺的對齊策略。
但正因為我們沒注意這個問題,導(dǎo)致編輯器對數(shù)據(jù)存放做了對齊,而我們?nèi)绻涣私獾脑?,就會對一些問題感到迷惑。
所以知其然,更要知其所以然。好了,我們介紹到這里,下一期再見!
免責(zé)聲明:本文內(nèi)容由21ic獲得授權(quán)后發(fā)布,版權(quán)歸原作者所有,本平臺僅提供信息存儲服務(wù)。文章僅代表作者個人觀點(diǎn),不代表本平臺立場,如有問題,請聯(lián)系我們,謝謝!