我們先來看一個(gè)例子

在例子中有一個(gè)
struct1結(jié)構(gòu)體,內(nèi)部有一個(gè)double類型和一個(gè)int類型的數(shù)據(jù),按理來說double類型占8字節(jié),int類型占4字節(jié),struct1結(jié)構(gòu)體的大小應(yīng)該是12字節(jié)才對,但是我們通過sizeof函數(shù)打印獲取struct1的占用的內(nèi)存大小時(shí)得到的卻是16字節(jié),多花銷的4字節(jié)的內(nèi)存就是內(nèi)存對齊導(dǎo)致的。
目錄
- 什么是內(nèi)存對齊
- 為什么要內(nèi)存對齊
- 內(nèi)存對齊的規(guī)則
什么是內(nèi)存對齊
計(jì)算機(jī)中內(nèi)存空間是按照byte劃分的,從理論上講似乎對任何類型的變量的訪問可以從任何地址開始,但實(shí)際情況是,在訪問特定類型變量的時(shí)候通常在特定的內(nèi)存地址訪問,這就需要對這些數(shù)據(jù)在內(nèi)存中存放的位置有限制,各種類型數(shù)據(jù)按照一定的規(guī)則在空間上排列,而不是順序的一個(gè)接一個(gè)的排列,這就是對齊。
內(nèi)存對齊是編譯器的管轄范圍。表現(xiàn)為:編譯器為程序中的的每個(gè)"數(shù)據(jù)單元"安排在合適的位置上。
為什么要內(nèi)存對齊
為了解釋這個(gè)問題,我們要先了解一下處理器是如何讀取內(nèi)存的。
如果我們吧內(nèi)存看做是簡單的字節(jié)數(shù)組,比如在C語言中,char *就可以表示一塊內(nèi)存。那么或許我們會(huì)認(rèn)為,它的內(nèi)存讀取方式可以按照1byte順序讀取,如下圖

然而,盡管內(nèi)存是以
字節(jié)為單位,但是大部分處理器并不是按照字節(jié)塊來存取內(nèi)存的,這取決于數(shù)據(jù)類型和處理器的設(shè)置;它一般會(huì)以2字節(jié),4字節(jié),8字節(jié),16字節(jié)甚至是32字節(jié)的塊來存取內(nèi)存,我們將上述這些存取單位稱為內(nèi)存存取粒度。

現(xiàn)在我們知道計(jì)算機(jī)的處理器是以一定大小的塊來進(jìn)行內(nèi)存讀取的,這作為我們的前提條件,那么為了解釋為什么要
內(nèi)存對齊,我們不妨先看一看不對齊的情況會(huì)出現(xiàn)什么問題。
對齊和數(shù)據(jù)在內(nèi)存中的位置有關(guān)。如果一個(gè)變量的內(nèi)存地址剛好位于它本身長度的整數(shù)倍的位置,他就被稱作自然對齊。例如一個(gè)整型變量(占4個(gè)字節(jié))的地址為0x16,那它就是自然對齊的。
現(xiàn)在假設(shè)一個(gè)整型變量(4字節(jié))不是自然對齊的,它的起始地址落在0x02位置,處理器想要訪問它的值,按照4字節(jié)的塊進(jìn)行讀取,從0x00起讀,讀取4字節(jié)大小,到0x03,這樣的一次讀取之后我們并不能取到我們要訪問的整型數(shù)據(jù),緊接著處理器會(huì)繼續(xù)再往下讀4字節(jié),從0x04開始到0x07為止,到這里處理器才能讀取到了我們需要訪問的內(nèi)存數(shù)據(jù),當(dāng)然這中間還存在剔除與合并的過程。

所以在此例子中,當(dāng)整型變量起始地址落在0x2時(shí),處理器需要
2次讀取才能取到我們要訪問的內(nèi)容,那如果數(shù)據(jù)時(shí)內(nèi)存對齊的呢?
顯然,如果數(shù)據(jù)時(shí)內(nèi)存對齊的,對于本例僅需讀取1次便可以讀取到目標(biāo)數(shù)據(jù)??梢?code>內(nèi)存對齊與否會(huì)影響到處理器的存取效率。同時(shí):
各個(gè)硬件平臺(tái)對存儲(chǔ)空間的處理上有很大的不同。一些平臺(tái)對某些
特定類型的數(shù)據(jù)只能從某些特定地址開始存取,而不是內(nèi)存中任意地址都是可以存取的。比如有些架構(gòu)的CPU在訪問一個(gè)沒有進(jìn)行對齊的變量的時(shí)候會(huì)發(fā)生錯(cuò)誤,那么在這種架構(gòu)下編程必須保證
內(nèi)存對齊。其他平臺(tái)可能沒有這種情況,但是最常見的是如果不按照適合其平臺(tái)要求對數(shù)據(jù)進(jìn)行對齊,會(huì)在存取效率上帶來損失。
也正是由于只能在特定的地址處存取數(shù)據(jù),所有在訪問一些數(shù)據(jù)時(shí),對于訪問未對齊的內(nèi)存,處理器可能需要進(jìn)行多次訪問;而對于對齊的內(nèi)存,只需要訪問一次就可以。這就是為什么要進(jìn)行內(nèi)存對齊的原因。
內(nèi)存對齊的原則
- 數(shù)據(jù)成員對齊規(guī)則:結(jié)構(gòu)(struct)(或聯(lián)合(union))的數(shù)據(jù)成員,第一個(gè)數(shù)據(jù)成員放在offset為0的地方,以后每個(gè)數(shù)據(jù)成員存儲(chǔ)的起始位置要從該成員大小或者成員的子成員大小(只要該成員有子成員,比如說是數(shù)組,結(jié)構(gòu)體等)的整數(shù)倍開始(比如int為4字節(jié),則要從4的整數(shù)倍地址開始存儲(chǔ))。
- 結(jié)構(gòu)體作為成員:如果一個(gè)結(jié)構(gòu)體中有某些結(jié)構(gòu)體成員,則結(jié)構(gòu)體成員要從其內(nèi)部最大元素大小的整數(shù)倍地址開始存儲(chǔ)(struct a里存有struct b,b里有char,int,double等元素,那b應(yīng)該從8的整數(shù)倍的地址開始存儲(chǔ))。
-
收尾工作:結(jié)構(gòu)體的總大小,也就是sizeof的結(jié)果,必須是其內(nèi)部最大成員的整數(shù)倍,不足的要補(bǔ)齊。
看文字規(guī)則有點(diǎn)不明所以,下面幾個(gè)例子可以更好的幫助理解。

在上面的例子中我們創(chuàng)建了一個(gè)結(jié)構(gòu)體,同時(shí)打印了它的內(nèi)存大小為24,那這個(gè)24是如何計(jì)算得來的呢?

按照規(guī)則
a占4個(gè)字節(jié)的存儲(chǔ)空間,同時(shí)從0x0的位置開始,所以0x0到0x4的位置存儲(chǔ)a的數(shù)據(jù),b占8個(gè)字節(jié)的存儲(chǔ)空間,本來應(yīng)該從0x4開始,但由于0x4不是8的整數(shù)倍,故0x4到0x7的位置空出,從0x8開始存儲(chǔ)b,一直到0xf,c占2個(gè)字節(jié)的存儲(chǔ)空間,0x10正好可以被2整除,故c存放在0x10和0x11的位置,同時(shí)根據(jù)規(guī)則3,結(jié)構(gòu)體的大小需要是內(nèi)部最大數(shù)據(jù)的整數(shù)倍,故struct1的大小應(yīng)該是8的倍數(shù),故0x12到0x17補(bǔ)空。至此整個(gè)struct1的大小為0x0到0x17,共計(jì)24個(gè)字節(jié)。

在struct2中我們將struct1中a和b的位置互換了一下,結(jié)果卻得到struct2的內(nèi)存大小只有16個(gè)字節(jié),此時(shí)我們再來看下struct2的內(nèi)存分布。

在
struct2中我們首先從0x0開始存放b,占用8個(gè)字節(jié)到0x7,接下來存放a,由于0x8正好可以被4整除,a便存儲(chǔ)在0x8到0xb的位置上,c占用2個(gè)字節(jié)同時(shí)0xc正好可以被2整除,c便存儲(chǔ)在0xc和0xd的位置上,同時(shí)根據(jù)規(guī)則3,結(jié)構(gòu)體的大小需要是內(nèi)部最大數(shù)據(jù)的整數(shù)倍,故struct2的大小應(yīng)該是8的倍數(shù),故0xe和0xf補(bǔ)空。至此整個(gè)struct2的大小為0x0到0xf,共計(jì)16個(gè)字節(jié)。

在struct3中我們將SLStruct2類型的結(jié)構(gòu)體當(dāng)做一個(gè)結(jié)構(gòu)體數(shù)據(jù),得到的結(jié)果是需要40個(gè)字節(jié)的內(nèi)存空間。此時(shí)我們來看下struct3的內(nèi)存分布情況。

在
struct3中0x0到0xd的內(nèi)存分布和struct2的內(nèi)存分布一樣,接下來的數(shù)據(jù)stru2是SLStruct2類型的,故其起始位置需要是SLStruct2中最大數(shù)據(jù)類型的整數(shù)倍,即double類型(8字節(jié))的整數(shù)倍,0xe和0xf不符合要求,故補(bǔ)空后從0x10開始存放stru2,stru2的內(nèi)存分布和struct2的內(nèi)存分布完全一樣,占用從0x10到0x1f位置16個(gè)字節(jié)的空間,然后d需要2個(gè)字節(jié)的空間并且0x20正好可以被2整除,d占用0x20和0x21兩個(gè)位置,同時(shí)根據(jù)規(guī)則3,結(jié)構(gòu)體的大小需要是內(nèi)部最大數(shù)據(jù)的整數(shù)倍(注意這里即使嵌套結(jié)構(gòu)體,也只需計(jì)算結(jié)構(gòu)體內(nèi)部最大數(shù)據(jù)的大小的整數(shù)倍,不需計(jì)算整個(gè)結(jié)構(gòu)體的大?。?,故struct3的大小應(yīng)該是8的倍數(shù),故0x22到0x27補(bǔ)空。至此整個(gè)struct3的大小為0x0到0x27,共計(jì)40個(gè)字節(jié)。
以上幾個(gè)例子應(yīng)該可以幫助我們更好的理解內(nèi)存對齊的規(guī)則。
其實(shí)內(nèi)存對齊就是制定了一套規(guī)則以合理的利用內(nèi)存空間并提高內(nèi)存訪問效率。編譯器通過適當(dāng)增加padding,是每個(gè)成員的方位都在一個(gè)指令里完成,而不需要多次訪問再拼接。是一個(gè)以空間換時(shí)間的過程。
從以上的例子也可以看出結(jié)構(gòu)體占用的內(nèi)存大小和數(shù)據(jù)成員的排列順序有關(guān),最大的數(shù)據(jù)成員放在前面可以有效的節(jié)約內(nèi)存。但是在實(shí)際書寫代碼的時(shí)候不可能按照數(shù)據(jù)成員的占用大小來申明屬性,如果隨便寫的話可能會(huì)造成大量的內(nèi)存浪費(fèi),那蘋果又是如何解決這個(gè)問題的呢?


在上圖中我們創(chuàng)建了一個(gè)SLPerson類型的對象,同時(shí)給person的各個(gè)屬性賦值,按照
SLPerson類的聲明來看各個(gè)屬性的聲明是沒有規(guī)則的,內(nèi)存的浪費(fèi)應(yīng)該很大。但是當(dāng)我們將程序運(yùn)行起來之后打印person的內(nèi)存分布時(shí)發(fā)現(xiàn)iOS幫助我們將屬性進(jìn)行了重新排列,將age和sex屬性存在了0x600001574608開始的8個(gè)字節(jié)內(nèi),將height和weight屬性存在了0x600001574610開始的8個(gè)字節(jié)內(nèi),name和engName各占8個(gè)字節(jié),內(nèi)存被有效利用了起來。以上就是蘋果幫助我們進(jìn)行的內(nèi)存優(yōu)化,即屬性重排。
總結(jié)
蘋果的內(nèi)存管理不僅僅有內(nèi)存對齊,還會(huì)通過屬性重排的方式幫助我們優(yōu)化內(nèi)存,減小內(nèi)存的開銷。
最后我們看下蘋果獲取內(nèi)存大小的三種方式:sizeof,class_getInstanceSize,malloc_size。
sizeof
sizeof傳入的主要對像是數(shù)據(jù)類型,最終得到的是該數(shù)據(jù)類型占用空間的大?。ㄈ?code>int,double,struct),如果傳入的是實(shí)例對象,其對象類型的本質(zhì)就是一個(gè)結(jié)構(gòu)體(即struct objc_object)的指針,所以sizeof返回的是對象指針的大小,而一個(gè)對象指針的大小固定是8個(gè)字節(jié)。
class_getInstanceSize
class_getInstanceSize是runtime提供的api,用于獲取類的實(shí)例對象所占用的內(nèi)存大?。?字節(jié)對齊),并返回具體的字節(jié)數(shù),其本質(zhì)就是獲取實(shí)例對象中成員變量的內(nèi)存大小。
malloc_size
malloc_size是獲取系統(tǒng)實(shí)際分配的內(nèi)存大?。?6字節(jié)對齊)。
例如,我們申明如下SLPerson類:
@interface SLPerson : NSObject
@property (nonatomic, assign) int age;
@property (nonatomic, copy) NSString *name;
@end
在main函數(shù)中創(chuàng)建SLPerson的示例對象person,并通過三種方式獲取person的內(nèi)存大小
#import <UIKit/UIKit.h>
#import "AppDelegate.h"
#import <objc/runtime.h>
#import <malloc/malloc.h>
#import "SLTeacher.h"
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
@autoreleasepool {
SLPerson *person = [[SLPerson alloc] init];
NSLog(@"sizeof(person) = %lu", sizeof(person));
NSLog(@"class_getInstanceSize(person) = %lu", class_getInstanceSize([person class]));
NSLog(@"malloc_size(person) = %lu", malloc_size((__bridge const void *)(person)));
}
return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
打印結(jié)果如下
2021-01-21 15:29:30.252973+0800 test[3449:221231] sizeof(person) = 8
2021-01-21 15:29:30.253448+0800 test[3449:221231] class_getInstanceSize(person) = 24
2021-01-21 15:29:30.253540+0800 test[3449:221231] malloc_size(person) = 32
由打印可看到
sizeof(person)獲取到的是一個(gè)指針的大小8字節(jié),
class_getInstanceClass([person class])獲取到的是SLPerson類型的對象實(shí)際需要的內(nèi)存大小,即其屬性所需的內(nèi)存大小isa(8字節(jié))+name(8字節(jié))+age(4字節(jié))+8字節(jié)內(nèi)存對齊,共需24字節(jié),
malloc_size(person)獲取到的是系統(tǒng)實(shí)際分配給person對象的內(nèi)存大小,iOS中系統(tǒng)的內(nèi)存對齊方式為16字節(jié)對齊,即系統(tǒng)每次訪問內(nèi)存的內(nèi)存存取粒度為16字節(jié),故在24字節(jié)的基礎(chǔ)上又補(bǔ)了8字節(jié)的空位,共計(jì)32字節(jié)。
本文參考自:關(guān)于內(nèi)存對齊,看我