1. 什么是內(nèi)存對齊
看下面的小程序辕漂,理論上,int
占4 byte
尼摹,char占一個1 byte
见芹,那么將它們放到一個結(jié)構(gòu)體中應(yīng)該占4+1=5byte
剂娄,但是實際上,通過運行程序得到的結(jié)果是8 byte
玄呛,這就是內(nèi)存對齊所導(dǎo)致的阅懦。
struct Struct {
int a; // 4
char b; // 1
}struct4;
NSLog(@"%lu",sizeof(struct4)); // 輸出為 8
計算機中內(nèi)存空間都是按照 byte
劃分的,從理論上講似乎對任何類型的變量的訪問可以從任何地址開始徘铝,但是實際的計算機系統(tǒng)對基本類型數(shù)據(jù)在內(nèi)存中存放的位置有限制耳胎,它們會要求這些數(shù)據(jù)的首地址的值是某個數(shù)k
(通常它為4
或8
)的倍數(shù),這就是所謂的內(nèi)存對齊惕它。
2. 為什么要內(nèi)存對齊
平臺原因(移植原因):不是所有的硬件平臺都能訪問任意地址上的任意數(shù)據(jù)的场晶;某些硬件平臺只能在某些地址處取某些特定類型的數(shù)據(jù),否則拋出硬件異常怠缸。
計算機的處理器是以一定大小的塊來進行讀取的诗轻,這作為我們的前提條件。
對齊跟數(shù)據(jù)在內(nèi)存中的位置有關(guān)揭北。如果一個變量的內(nèi)存地址剛好位于它本身長度的整數(shù)倍扳炬,他就被稱做自然對齊。例如一個整型變量(占4字節(jié))的地址為0x00000008
搔体,那它就是自然對齊的恨樟。
現(xiàn)在假設(shè)一個整型變量(4字節(jié))不是自然對齊的,它的起始地址落在0x00000002
(圖中藍色區(qū)域)疚俱,處理器想要訪問它的值劝术,按照4字節(jié)的塊進行讀取,從圖中的0x00
起讀呆奕,讀取4字節(jié)大小养晋,讀到0x03
這樣的一次讀取之后,我們并不能取到我們要訪問的整型數(shù)據(jù)梁钾,緊接著處理器會繼續(xù)再往下讀绳泉,偏移4個字節(jié),從0x04
開始姆泻,讀到0x07
到這一步零酪,處理器才能讀取到了我們需要訪問的內(nèi)存數(shù)據(jù),當(dāng)然這中間還存在剔除與合并的過程拇勃。在上面的例子中四苇,要讀取兩次才能獲取到我們想要的數(shù)據(jù)。
那么如果是內(nèi)存對齊的呢方咆?
由上圖可知月腋,只要讀取一次就能獲取到相應(yīng)的數(shù)據(jù)。
因此可以得出,內(nèi)存是否對齊罗售,會影響到數(shù)據(jù)的讀取效率辜窑。另外不同的內(nèi)存存取粒度對同一任務(wù)也有不同影響钩述,這里我們不過多的討論寨躁。
3.內(nèi)存對齊原則
- 數(shù)據(jù)成員對?規(guī)則:結(jié)構(gòu)(
struct
)(或聯(lián)合(union
))的數(shù)據(jù)成員,第一個數(shù)據(jù)成員放在offset
為0的地方(即首地址的位置)牙勘,以后每個數(shù)據(jù)成員存儲的起始位置要從該成員大小或者成員的子成員大小(只要該成員有子成員职恳,比如說是數(shù)組,結(jié)構(gòu)體等)的整數(shù)倍開始(比如int
為4
字節(jié),則要從4
的整數(shù)倍地址開始存儲方面。- 結(jié)構(gòu)體作為成員:如果一個結(jié)構(gòu)里有某些結(jié)構(gòu)體成員,則結(jié)構(gòu)體成員要從其內(nèi)部最大元素大小的整數(shù)倍地址開始存儲.(
struct a
里存有struct b
,b
里有char
放钦、int
、double
等元素,那b
應(yīng)該從8
的整數(shù)倍開始存儲.)- 收尾工作:結(jié)構(gòu)體的總大小,也就是
sizeof
的結(jié)果,必須是其內(nèi)部最大成員的整數(shù)倍恭金,不足的要補?操禀。
按我自己的理解來看:
- 對于規(guī)則1 每個數(shù)據(jù)成員的起始位置,都是自身大小的整數(shù)倍横腿。
- 對于規(guī)則2 對于結(jié)構(gòu)體做為成員變量颓屑,起始位置要根據(jù)自身成員變量最大的元素來確定。
下面我們來看一個簡單的例子:
struct TestStruct {
double a; // 8
char b; // 1
int c; // 4
short d; // 2
}struct1;
double
的大小為8
個字節(jié),按照規(guī)則1耿焊, a
成員變量將會占據(jù)前首地址開始的 8
個字節(jié)揪惦。也就是0x00
到0x07
的地址。
char
的大小為1
,按照規(guī)則1罗侯,內(nèi)存中的第9
為是1
的倍數(shù)器腋,占據(jù)一個字節(jié)0x08
,單數(shù)成員變量c
,為int
類型,大小為4
钩杰,內(nèi)存中的第10
為并不是4
的倍數(shù)纫塌,所以并不能滿足規(guī)則1。因此讲弄,成員變量b
要補3
個位置护戳,即占據(jù)0x08
到0x11
的地址單元,成員變量c
從0x12
到0x15
開始占據(jù)4
個內(nèi)存單元垂睬。同理媳荒,按照規(guī)則1可以得出成員變量d
占據(jù)0x16
到0x08
的內(nèi)存單元,總共占據(jù)18
個字節(jié)驹饺。最后钳枕,我們進行收尾工作:結(jié)構(gòu)體的總大小,必須是其內(nèi)部最大成員的整數(shù)倍赏壹。
內(nèi)部最大成員為double
8
個字節(jié)鱼炒,可以計算出結(jié)構(gòu)的總大小為24
。
最后我們用代碼來驗證下:
如果結(jié)構(gòu)體中存在結(jié)構(gòu)體成員變量會怎樣蝌借?
struct TestStruct2 {
double a; // 8
int b; // 4
char c; // 1
short d; // 2
int e; // 4
struct TestStruct1 str;
}struct2;
按照規(guī)則1
昔瞧,可以得出前5個變量占據(jù)24
個內(nèi)存單元指蚁,對于結(jié)構(gòu)體成員str
,其內(nèi)部最大成員為double
8
個字節(jié)自晰,而24
為8
的倍數(shù)凝化,符合規(guī)則1
, str
占據(jù)后面24
個字節(jié)單元酬荞。此時搓劫,結(jié)構(gòu)體總共占據(jù)48
個內(nèi)存單元,按照規(guī)則3
TestStruct2
的最大成員是 TestStruct1 str
24
個字節(jié)混巧,符合規(guī)則枪向。
下面我么用代碼驗證下結(jié)果
4. 總結(jié)
其實,內(nèi)存對齊就是定制了一套規(guī)則咧党,以合理的利用內(nèi)存空間并提高內(nèi)存訪問效率秘蛔。 編譯器通過適當(dāng)增加padding
,使每個成員的訪問都在一個指令里完成傍衡,而不需要多次訪問再拼接深员。是一個以空間換時間
的過程。