引
先來看一下這個(gè)例子:
// 64位系統(tǒng)
#include<stdio.h>
struct{
int a;
char b;
}s;
int main ()
{
printf ("%d\n",sizeof(s);
return 0;
}
理論上,64位系統(tǒng)下党巾,int占 4個(gè)byte萎庭,char占 1個(gè)byte,那么將它們放到一個(gè)結(jié)構(gòu)體中應(yīng)該占 4+1 = 5byte齿拂;但是實(shí)際上驳规,通過運(yùn)行程序得到的結(jié)果是 8byte,這就是內(nèi)存對(duì)齊所導(dǎo)致的创肥。
注:本文討論的內(nèi)容均是在64位系統(tǒng)下。
大綱
什么是內(nèi)存對(duì)齊
為什么要內(nèi)存對(duì)齊
內(nèi)存對(duì)齊規(guī)則
一、什么是內(nèi)存對(duì)齊
計(jì)算機(jī)中內(nèi)存空間是按照byte劃分的叹侄,從理論上講似乎對(duì)任何類型的變量的訪問可以從任何地址開始巩搏,但實(shí)際情況是:在訪問特定類型變量
的時(shí)候通常在特定的內(nèi)存地址
訪問,這就需要對(duì)這些數(shù)據(jù)在內(nèi)存中存放的位置有限制趾代,各種類型數(shù)據(jù)按照一定的規(guī)則在空間上排列贯底,而不是順序的一個(gè)接一個(gè)的排放,這就是對(duì)齊撒强。
內(nèi)存對(duì)齊是編譯器的管轄范圍禽捆。表現(xiàn)為:編譯器為程序中的每個(gè)“數(shù)據(jù)單元”安排在適當(dāng)?shù)奈恢蒙稀?/p>
二、為什么要內(nèi)存對(duì)齊
為了解釋這個(gè)問題飘哨,我們先要了解一下處理器是如何讀取內(nèi)存的胚想?
我們?nèi)绻褍?nèi)存看做是簡(jiǎn)單的字節(jié)數(shù)組,比如在C語言中芽隆,char *就可表示一塊內(nèi)存浊服。那么或許我們會(huì)認(rèn)為,它的內(nèi)存讀取方式可以按照1byte順序讀取胚吁,如下圖牙躺。
然而,盡管內(nèi)存是以字節(jié)為單位腕扶,但是大部分處理器并不是按字節(jié)塊來存取內(nèi)存的孽拷,這取決于數(shù)據(jù)類型和處理器的設(shè)置;它一般會(huì)以雙字節(jié),四字節(jié),8字節(jié),16字節(jié)甚至32字節(jié)的塊
來存取內(nèi)存半抱,我們將上述這些存取單位稱為內(nèi)存存取粒度
.
現(xiàn)在我們知道脓恕,計(jì)算機(jī)的處理器是以一定大小的塊來進(jìn)行讀取的,這作為我們的前提條件代虾,那么為了解釋為什么要內(nèi)存對(duì)齊进肯?我們不妨先看一看不對(duì)齊的情況會(huì)出現(xiàn)什么問題?
對(duì)齊跟數(shù)據(jù)在內(nèi)存中的位置有關(guān)棉磨。如果一個(gè)變量的內(nèi)存地址剛好位于它本身長(zhǎng)度的整數(shù)倍江掩,他就被稱做自然對(duì)齊。例如一個(gè)整型變量(占4字節(jié))的地址為0x00000016乘瓤,那它就是自然對(duì)齊的环形。
現(xiàn)在假設(shè)一個(gè)整型變量(4字節(jié))不是自然對(duì)齊的,它的起始地址落在0x00000002(圖中藍(lán)色區(qū)域)衙傀,處理器想要訪問它的值抬吟,按照4字節(jié)的塊進(jìn)行讀取,從圖中的0x0起讀统抬,讀取4字節(jié)大小火本,讀到0x3
這樣的一次讀取之后危队,我們并不能取到我們要訪問的整型數(shù)據(jù),緊接著處理器會(huì)繼續(xù)再往下讀钙畔,偏移4個(gè)字節(jié)茫陆,從0x4開始,讀到0x7
到這里擎析,處理器才能讀取到了我們需要訪問的內(nèi)存數(shù)據(jù)簿盅,當(dāng)然這中間還存在剔除與合并的過程。
所以揍魂,在此例中桨醋,當(dāng)整型變量起始地址落在0x2時(shí)(不對(duì)齊),處理器需要兩次讀取才能取到我們要訪問的內(nèi)容现斋。
那如果是對(duì)齊的呢喜最?
顯然,如果是對(duì)齊的步责,對(duì)于本例返顺,僅需讀取1次,我們便可以讀取到目標(biāo)數(shù)據(jù)蔓肯。
可見遂鹊,對(duì)齊與否會(huì)影響到我們的讀取效率。
同時(shí):
各個(gè)硬件平臺(tái)對(duì)存儲(chǔ)空間的處理上有很大的不同蔗包。一些平臺(tái)對(duì)某些
特定類型的數(shù)據(jù)
只能從某些特定地址
開始存取,而不是內(nèi)存中任意地址都是可以讀取的秉扑。比如有些架構(gòu)的CPU在訪問一個(gè)沒有進(jìn)行對(duì)齊的變量的時(shí)候會(huì)發(fā)生錯(cuò)誤,那么在這種架構(gòu)下編程必須保證字節(jié)對(duì)齊.其他平臺(tái)可能沒有這種情況,但是最常見的是如果不按照適合其平臺(tái)要求對(duì)數(shù)據(jù)存放進(jìn)行對(duì)齊调限,會(huì)在存取效率上帶來損失舟陆。
也正是由于只能在特定的地址處讀取數(shù)據(jù),所以在訪問一些數(shù)據(jù)時(shí)耻矮,對(duì)于訪問未對(duì)齊的內(nèi)存秦躯,處理器可能需要進(jìn)行多次訪問;而對(duì)于對(duì)齊的內(nèi)存裆装,只需要訪問一次就可以踱承。
這就是為什么要內(nèi)存對(duì)齊的原因。
內(nèi)存對(duì)齊不僅便于CPU快速訪問哨免,同時(shí)合理的利用字節(jié)對(duì)齊可以有效地節(jié)省存儲(chǔ)空間茎活。
我們可以同時(shí)對(duì)比一下,不同內(nèi)存存取粒度對(duì)同一任務(wù)的不同影響琢唾。
設(shè)定一個(gè)相同的任務(wù):分別從Address0 和 Address1 中從地址0讀取4個(gè)字節(jié)到處理器的寄存器中载荔。
- 先看單字節(jié)粒度情況
兩圖中左側(cè)表示內(nèi)存,右側(cè)表示寄存器采桃,中間的箭頭表示讀取的過程懒熙。因?yàn)槭菃巫止?jié)存取粒度丘损,讀取內(nèi)存是按照1個(gè)字節(jié)進(jìn)行訪問的,所以對(duì)于Address0 來講工扎,要想從0的位置讀取4個(gè)字節(jié)号俐,需要讀取4次,對(duì)于 Address1 也是一樣的定庵。即使他不是內(nèi)存對(duì)齊的也無妨。
- 再看雙字節(jié)粒度情況
從Address0 讀取4個(gè)字節(jié)踪危,相比于存取粒度為1字節(jié)的處理器蔬浙,存取次數(shù)變成了一半,只需讀取2次贞远。由于每個(gè)內(nèi)存訪問都需要固定的開銷畴博,因此最小化訪問次數(shù)確實(shí)可以提高性能。同時(shí)Address0 是內(nèi)存對(duì)齊的(數(shù)據(jù)的起始位置落在0的位置上)蓝仲,所以第一次讀取地址01俱病,第二次讀取地址23即可取到目標(biāo)數(shù)據(jù)。
但是袱结,從Address1 讀取時(shí)亮隙。由于該地址未均勻地落在處理器的內(nèi)存訪問邊界上(Address1 中寄存器黑框區(qū)域是該數(shù)據(jù)的內(nèi)存地址區(qū)域,起始位置為1垢夹,不是對(duì)齊的)溢吻,該處理器去取數(shù)據(jù)時(shí),要先從0地址開始讀取第一個(gè)2字節(jié)塊(01)果元,剔除不想要的字節(jié)(0地址)促王,然后從地址2開始讀取下一個(gè)2字節(jié)塊(23),再從地址4開始讀取下一個(gè)2字節(jié)塊(45)而晒,剔除不想要的字節(jié)(地址5)蝇狼。這樣讀取3次之后將最后留下的3塊數(shù)據(jù)合并放入寄存器,才能取到目標(biāo)數(shù)據(jù)倡怎。
- 四字節(jié)粒度情況呢迅耘?
具有四字節(jié)粒度的處理器從Address0 讀取時(shí),可以一次讀取地址0123 就從對(duì)齊的地址中提取四個(gè)字節(jié)诈胜。
然而從Address1 讀取時(shí)豹障,因?yàn)槭遣粚?duì)齊的,讀取地址0123焦匈,剔除0地址血公,繼而讀取地址4567,剔除地址5缓熟、地址6累魔、地址7 摔笤,這樣讀取2次后將留下的2塊數(shù)據(jù)合并放入寄存器,取到目標(biāo)數(shù)據(jù)垦写。
從雙字節(jié)粒度和四字節(jié)粒度中可見吕世,對(duì)于沒有對(duì)齊的內(nèi)存,需要做更多次的讀取與剔除和合并的過程梯投。這顯然是降低效率的命辖。
同時(shí)我們還需要注意到一點(diǎn):對(duì)于對(duì)齊的內(nèi)存,不同的存取粒度也會(huì)影響到存取效率分蓖。粒度小則存取次數(shù)多尔艇,粒度大則浪費(fèi)空間。所以在每個(gè)特定平臺(tái)上的編譯器都有自己的默認(rèn)存取粒度么鹤。
了解了內(nèi)存對(duì)齊以及原因终娃,我們繼續(xù)看一下內(nèi)存對(duì)齊的原則是什么。
三蒸甜、內(nèi)存對(duì)齊規(guī)則
對(duì)于標(biāo)準(zhǔn)數(shù)據(jù)類型
它的地址只要是它的長(zhǎng)度的整數(shù)倍就行了
對(duì)于結(jié)構(gòu)體
在結(jié)構(gòu)體中棠耕,編譯器為結(jié)構(gòu)體的每個(gè)成員按其自然邊界(alignment)分配空間。各個(gè)成員按照它們被聲明的順序在內(nèi)存中順序存儲(chǔ)柠新,第一個(gè)成員的地址和整個(gè)結(jié)構(gòu)體的地址相同窍荧。具體規(guī)則如下:
1.第一個(gè)成員在結(jié)構(gòu)體變量偏移量為0 的地址處,也就是第一個(gè)成員必須從頭開始恨憎。
2.以后每個(gè)成員相對(duì)于結(jié)構(gòu)體首地址的 offset 都是該成員大小的整數(shù)倍搅荞,如有需要編譯器會(huì)在成員之間加上填充字節(jié)。
3.結(jié)構(gòu)體的總大小為 最大對(duì)齊數(shù)的整數(shù)倍(每個(gè)成員變量都有自己的對(duì)齊數(shù))框咙,如有需要編譯器會(huì)在最末一個(gè)成員之后加上填充字節(jié)咕痛。
4.如果嵌套結(jié)構(gòu)體,嵌套的結(jié)構(gòu)體對(duì)齊到自己的最大對(duì)齊數(shù)的整數(shù)倍處喇嘱,結(jié)構(gòu)體的整體大小就是所有最大對(duì)齊數(shù)(包含嵌套結(jié)構(gòu)體的對(duì)齊數(shù))的整數(shù)倍茉贡。
搞個(gè)例子嘗嘗
示例1:
struct test_t {
int a;
long b;
short c;
};
第一個(gè)成員為int類型,占4字節(jié)者铜,內(nèi)存分布 00 01 02 03 腔丧;用紅色表示
第二個(gè)成員為long類型,占8字節(jié)作烟,此時(shí)內(nèi)存的的偏移量04不是8的整數(shù)倍愉粤,所以要填充字節(jié)(綠色表示填充字節(jié)),到0x7的位置拿撩,然后將long類型的數(shù)據(jù)b寫入內(nèi)存(黃色)
第三個(gè)成員是short類型衣厘,占2個(gè)字節(jié),此時(shí)內(nèi)存的偏移量16是short類型所占字節(jié)數(shù)的整數(shù)倍,所以直接寫入內(nèi)存(用藍(lán)色表示)
至此影暴,結(jié)構(gòu)體內(nèi)的數(shù)據(jù)數(shù)據(jù)成員已對(duì)齊错邦,但是當(dāng)前結(jié)構(gòu)體的總大小為18,不滿足規(guī)則3型宙,所以需在最末的成員后面填充6個(gè)字節(jié)撬呢,使其總大小為24。
所以 此結(jié)構(gòu)體的所占內(nèi)存大小為24妆兑。
如果將結(jié)構(gòu)體中的short類型與long類型換一下魂拦,
struct test_t {
int a;
short b;
long c;
};
會(huì)是一個(gè)什么結(jié)果?我們依然用紅色表示int類型搁嗓,藍(lán)色表示short類型晨另,黃色表示long類型,綠色表示填充字節(jié)谱姓。
結(jié)果為16∨偾纾可見屉来,對(duì)于成員相同的結(jié)構(gòu)體,如果改變成員的順序狈癞,對(duì)于結(jié)構(gòu)體所占空間的大小是會(huì)產(chǎn)生影響的茄靠,所以,我們不但要了解內(nèi)存對(duì)齊蝶桶,還是正確的利用內(nèi)存對(duì)齊慨绳。
對(duì)于結(jié)構(gòu)體嵌套結(jié)構(gòu)體,在規(guī)則4中已經(jīng)給出真竖,這里的圖就不再畫了脐雪,聰明的你看到這里一定可以得到正確的答案。
我們?cè)賮砘仡櫳厦娴膶?duì)齊規(guī)則:
各成員變量存放的起始地址恢共, 相對(duì)于結(jié)構(gòu)的起始地址的偏移量 战秋,必須為該變量的類型所占用的字節(jié)數(shù)的倍數(shù);
各成員變量在存放的時(shí)候根據(jù)在結(jié)構(gòu)中出現(xiàn)的順序依次申請(qǐng)空間讨韭, 同時(shí)按照上面的對(duì)齊方式調(diào)整位置脂信, 空缺的字節(jié)自動(dòng)填充
同時(shí)為了確保結(jié)構(gòu)的大小為結(jié)構(gòu)的字節(jié)邊界數(shù)(即該結(jié)構(gòu)中占用最大的空間的類型的字節(jié)數(shù))的倍數(shù),所以在為最后一個(gè)成員變量申請(qǐng)空間后 還會(huì)根據(jù)需要自動(dòng)填充空缺的字節(jié)
這就是內(nèi)存對(duì)齊的整體規(guī)則透硝,但仍需注意的是:
在不同架構(gòu)的處理器下狰闪,我們運(yùn)行同一個(gè)示例得到的結(jié)果可能不同,甚至不同的編譯器配置濒生,也會(huì)影響這個(gè)結(jié)果埋泵,我們需要了解影響內(nèi)存對(duì)齊規(guī)則的因素有哪些?
影響內(nèi)存對(duì)齊結(jié)果的因素 罪治?
1.#pragma pack(n)
每個(gè)特定平臺(tái)上的編譯器都有自己的默認(rèn)“對(duì)齊系數(shù)”(也叫對(duì)齊模數(shù))秋泄。程序員可以通過預(yù)編譯命令#pragma pack(n)
琐馆,n=1,2,4,8,16來改變這一系數(shù),其中的n就是你要指定的“對(duì)齊系數(shù)”恒序。這里規(guī)定的是上界瘦麸,只影響對(duì)齊單元大于n的成員,對(duì)于對(duì)齊字節(jié)不大于n的成員沒有影響歧胁。
可以認(rèn)為處理器一次性可以從內(nèi)存中讀/寫n個(gè)字節(jié)滋饲。對(duì)于大小小于n的成員,按照自己的對(duì)齊條件對(duì)齊喊巍,因?yàn)椴徽撛趺捶哦伎梢砸淮涡匀〕鐾犁浴?duì)于對(duì)齊條件大于n個(gè)字節(jié)的成員,成員按照自身的對(duì)齊條件對(duì)齊和按照n字節(jié)對(duì)齊需要相同的讀取次數(shù)崭参,但按照n字節(jié)對(duì)齊節(jié)省空間呵曹。
通過預(yù)編譯命令#pragma pack()
取消自定義字節(jié)對(duì)齊方式。
也可以寫成:
#pragma pack(push,n)
#pragma pack(pop)
2.__attribute__((aligned (n)))
__attribute__((aligned (n)))
何暮,讓所作用的結(jié)構(gòu)成員對(duì)齊在n字節(jié)自然邊界上奄喂。如果結(jié)構(gòu)中有成員的長(zhǎng)度大于n,則按照最大成員的長(zhǎng)度來對(duì)齊海洼。
__attribute__((packed))
跨新,取消結(jié)構(gòu)在編譯過程中的優(yōu)化對(duì)齊,按照實(shí)際占用字節(jié)數(shù)進(jìn)行對(duì)齊坏逢。
需要注意的是:內(nèi)存對(duì)齊的 對(duì)齊數(shù) 取決于 對(duì)齊系數(shù) 和 成員的字節(jié)數(shù) 兩者之中的較小值域帐。
舉例說明:
struct test
{
char x1;
short x2;
float x3;
char x4;
}
默認(rèn)情況下,結(jié)構(gòu)的第一個(gè)成員x1,其偏移地址為0是整,占據(jù)了第1個(gè)字節(jié)肖揣。第二個(gè)成員x2為short類型,其起始地址必須2字節(jié)對(duì)界浮入,因此许饿,編譯器在x2和x1之間填充了一個(gè)空字節(jié)。結(jié)構(gòu)的第三個(gè)成員x3和第四個(gè)成員x4恰好落在其自然邊界地址上舵盈,在它們前面不需要額外的填充字節(jié)陋率。在test結(jié)構(gòu)中,成員x3要求4字節(jié)對(duì)齊秽晚,是該結(jié)構(gòu)所有成員中要求的最大邊界單元瓦糟,因而test結(jié)構(gòu)的自然對(duì)齊條件為4字節(jié),編譯器在成員x4后面填充了3個(gè)空字節(jié)赴蝇。整個(gè)結(jié)構(gòu)所占據(jù)空間為12字節(jié)菩浙。
當(dāng)使用:#pragma pack(1)
//讓編譯器對(duì)這個(gè)結(jié)構(gòu)作1字節(jié)對(duì)齊
#pragma pack(1) //讓編譯器對(duì)這個(gè)結(jié)構(gòu)作1字節(jié)對(duì)齊
struct test
{
char x1;
short x2;
float x3;
char x4;
};
#pragma pack() //取消1字節(jié)對(duì)齊,恢復(fù)為默認(rèn)4字節(jié)對(duì)齊
這時(shí)候sizeof(struct test)的值為8。
同理:使用__attribute__((packed))
#define PACKED __attribute__((packed))
struct PACKED test
{
char x1;
short x2;
float x3;
char x4;
}test;
這時(shí)候sizeof( test)的值仍為8劲蜻。
總結(jié)
其實(shí)陆淀,內(nèi)存對(duì)齊就是定制了一套規(guī)則,以合理的利用內(nèi)存空間并提高內(nèi)存訪問效率先嬉。
編譯器通過適當(dāng)增加padding轧苫,使每個(gè)成員的訪問都在一個(gè)指令里完成,而不需要多次訪問再拼接疫蔓。
是一個(gè)以空間換時(shí)間的過程含懊。