在iOS底層原理--alloc&init&new這篇文章中浦辨,我們認識到了字節(jié)對齊曹体。
那么仆百,我們回顧一下什么是字節(jié)對齊驶社。
字節(jié)對齊
假如一個創(chuàng)建一個對象LGPerson
//創(chuàng)建LGPerson
LGPerson *p1 = [LGPerson alloc];
通過調(diào)用
//獲取size
size = cls->instanceSize(extraBytes);
之后得到size = 16
。
接下來我們調(diào)用class_getInstanceSize
(這個方法是獲取類對象的實際內(nèi)存)
// size = 8
size_t size = class_getInstanceSize([LGPerson class])
最終得到的size = 8
這就驗證了字節(jié)對齊的存在,而且由于實際大小為8涵防,所以字節(jié)對齊的最小值是16個字節(jié)闹伪。
內(nèi)存對齊
很多 CPU拒絕讀取未對齊數(shù)據(jù)。當一個程序要求這些 CPU 讀取未對齊數(shù)據(jù)時武学,這時 CPU 會進入異常處理狀態(tài)并且通知程序不能繼續(xù)執(zhí)行祭往。而且讀取未對齊的數(shù)據(jù),會大大降低 CPU 的性能火窒。
CPU把內(nèi)存當成是一塊一塊的,塊的大小可以是2驮肉,4熏矿,8,16字節(jié)大小离钝,因此CPU在讀取內(nèi)存時是一塊一塊進行讀取的票编。每次內(nèi)存存取都會產(chǎn)生一個固定的開銷,減少內(nèi)存存取次數(shù)將提升程序的性能卵渴。所以 CPU 一般會以 2/4/8/16/32 字節(jié)為單位來進行存取操作慧域。我們將上述這些存取單位也就是塊大小稱為(memory access granularity)內(nèi)存存取粒度。
我們用一段代碼來解釋內(nèi)存對齊
浪读。
- 首先我們定義2個結(jié)構(gòu)體
struct LGStruct1 {
double a; //占8位
char b; //占1位
int c; //占4位
short d; //占2位
}struct1;
struct LGStruct2 {
double a; //占8位
int b; //占4位
char c; //占1位
short d; //占2位
}struct2;
接下來用sizeof(strut)
打印結(jié)構(gòu)體的內(nèi)存大小昔榴,照理說2個結(jié)構(gòu)體的內(nèi)容一樣,只是排布順序不同碘橘,大小應(yīng)該一致互订。那么打印結(jié)果如下:
明顯看到,2個結(jié)構(gòu)體的內(nèi)存大小不一樣痘拆,這就是
內(nèi)存對齊
產(chǎn)生的影響仰禽。
內(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ù)倍.不?的要補?温峭。
內(nèi)存對齊的解釋
上面的話太官方了,我們用比較通俗的語言來解釋识颊。
針對上面struct1
這個案例來分析诚镰,首先我們已經(jīng)標注出每個成員所占的內(nèi)存大小奕坟。
struct LGStruct1 {
double a; //占8位
char b; //占1位
int c; //占4位
short d; //占2位
}struct1;
具體類型在c/oc中所占內(nèi)存大小如圖:
所以按照規(guī)則來進行操作:
-
第?個數(shù)據(jù)成員放在offset為0的地?,我們從0開始,
double 占8個字節(jié)
double a.png
如上圖,a從0開始清笨,長度為8月杉,所以截止到7。
-
每個數(shù)據(jù)成員存儲的起始位置要從該成員??或者成員的?成員??抠艾,比如第二個成員為
char
,char的長度為1苛萎,所以從8開始即可
char b.png
如上圖,b從8開始检号,長度為1腌歉,所以截止到9 -
還是上面的規(guī)則,第三個成員為
int
,int的長度為4齐苛,所以要從4的倍數(shù)的位置開始翘盖,而鄰近的4的倍數(shù)的位置為12,所以從12起始
int c.png
如上圖凹蜂,c從12開始馍驯,長度為4,所以截止到15 -
第四個成員為
short
,short的長度為2玛痊,所以要從2的倍數(shù)的位置開始汰瘫,而鄰近的2的倍數(shù)的位置為16,所以從16起始
short d.png
如上圖擂煞,d16開始混弥,長度為2,所以截止到17对省。 -
最后蝗拿,結(jié)構(gòu)體的總??,也就是sizeof的結(jié)果,.必須是其內(nèi)部最?成員的整數(shù)倍。不?的要補?官辽。對于上面的
struct1
蛹磺,內(nèi)部最大成員為a = 8
,它的整數(shù)倍為8 * 3 = 24
。
最終長度.png
所以最終長度為24同仆。
接下來萤捆,我們用上面的規(guī)則來看下struct2
這個例子。
struct LGStruct2 {
double a; //占8位
int b; //占4位
char c; //占1位
short d; //占2位
}struct2;
還是通過示意圖來來解讀俗批。
最終得出了struct2的長度為16俗或。
結(jié)構(gòu)體的嵌套
接下來,我們來下面這個例子岁忘,結(jié)構(gòu)體struct2
中嵌套了一個結(jié)構(gòu)體struct1
辛慰。
struct Struct1 {
double a; //占8位
char b; //占1位
int c; //占4位
short d; //占2位
}struct1;
struct Struct2 {
double e; //占8位
int f; //占4位
char g; //占1位
short h; //占2位
struct Struct1 I;
}struct2;
- 首先排列
double e
,int f
,char g
,short h
,如下圖:
結(jié)構(gòu)體.png - 接下來嵌入體,規(guī)則為結(jié)構(gòu)體作為成員:如果?個結(jié)構(gòu)?有某些結(jié)構(gòu)體成員,則結(jié)構(gòu)體成員要從其內(nèi)部最?元素??的整數(shù)倍地址開始存儲干像。那么針對嵌套的
struct1
來說帅腌,里面最大的元素為double a
,長度為8
驰弄。所以最臨近的點為16,所以結(jié)構(gòu)體從16開始速客。
結(jié)構(gòu)體.png
如上圖所示戚篙,實際需要的內(nèi)存大小為33個字節(jié)。 -
最后溺职,結(jié)構(gòu)體的總??,也就是sizeof的結(jié)果,.必須是其內(nèi)部最?成員的整數(shù)倍岔擂。不?的要補?。按照最大長度的整數(shù)倍浪耘,所以計算得出
8 * 5 = 40
乱灵,最終長度為40個字節(jié)。
最終長度.png
所以七冲,再看內(nèi)存對齊
的規(guī)則會更加清晰痛倚。
- 數(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ù)倍.不?的要補?。
屬性重排(內(nèi)存優(yōu)化)
我們知道削锰,所有的oc對象本質(zhì)上是一個結(jié)構(gòu)體
通铲,那么如果按照內(nèi)存對齊
對齊的原則的話,我們一定要特別注意屬性存放的位置器贩。因為結(jié)構(gòu)體的大小颅夺,和結(jié)構(gòu)體成員的排列順序有關(guān)。
但是實際上蛹稍,并沒有人去關(guān)注對象屬性的位置吧黄。這就是屬性重排
的作用。
通過一個例子來看唆姐。
- 我們定義一個對象
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface LGPerson : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *nickName;
@property (nonatomic, copy) NSString *hobby;
@property (nonatomic, assign) int age;
@property (nonatomic, assign) long height;
@property (nonatomic) char c1;
@property (nonatomic) char c2;
@end
NS_ASSUME_NONNULL_END
- 我們給它賦值
//賦值
LGPerson *person = [LGPerson alloc];
person.name = @"Cooci";
person.nickName = @"KC";
person.age = 18;
person.c1 = 'a';
person.c2 = 'b';
-
接下來打印數(shù)據(jù),
po
得出LGPerson
的內(nèi)存地址
po.png -
通過
x/4gx
找出LGPerson
的屬性地址
x/4gx.png -
我們分別打印4個內(nèi)存地址
打印內(nèi)存地址.png分別能得出
LGPerson
拗慨、KC
、Cooci
奉芦。但是77309436513
這一串是個亂碼 -
我們把
0x0000001200006261
拆分成0x00000012
,0x62
,0x61
,分別得到
image.png
可以得到18
,a
,b
了赵抢。
通過ASCII換算得出97為a,98為b。
我們用一張圖來表示
對象的內(nèi)存對齊
我們用一段代碼來展示,還是之前的對象声功,我們來打印一下LGPerson
LGPerson *person = [LGPerson alloc];
person.name = @"Cooci";
person.nickName = @"KC";
NSLog(@"%@ - %lu - %lu - %lu",person,sizeof(person),class_getInstanceSize([LGPerson class]),malloc_size((__bridge const void *)(person)));
打印結(jié)果如下:
對打印信息進行分析
NSLog(@"%@", person)
這個打印出來的結(jié)果就是LGPerson烦却,包括它的地址信息。NSLog(@"%lu",sizeof(person))
person
實際上是一個指向LGPerson
的指針先巴,一個指針有8個字節(jié)其爵,所以大小為8冒冬。不管LGPerson
有多大,person
的大小一直為8摩渺。NSLog(@"%lu",class_getInstanceSize([LGPerson class]))
class_getInstanceSize
這個方法的作用就是返回對象真正需要的內(nèi)存简烤,進行了長度為8的字節(jié)對齊
≈ぢ撸看源碼乐埠,點擊進去,實際走到了這個方法:
//定義WORD_MASK為7UL
# define WORD_MASK 7UL
static inline uint32_t word_align(uint32_t x) {
return (x + WORD_MASK) & ~WORD_MASK;
}
跟之前的分析方式一樣囚企,傳進去的x = 24
,WORD_MASK = 7
,相加后
// 7 + 31
x + WORD_MASK = 31
轉(zhuǎn)換成二進制
//31的二進制
0000 0000 0001 1111
~WORD_MASK
表示WORD_MASK
的二進制取反
//7的二進制取反
1111 1111 1111 1000
最后經(jīng)過與運算,得到
//31的二進制
0000 0000 0001 1111
//7的二進制取反
&1111 1111 1111 1000
//最終結(jié)果
= 0000 0000 0001 1000
可以看到丈咐,前面3位都抹0,所以可以得出結(jié)論龙宏,對象創(chuàng)建的時候棵逊,真正進行的是8位的字節(jié)對齊银酗。
- NSLog(@"%lu",malloc_size((__bridge const void *)(person)))
通過malloc
算法進行研究
目前已知的16字節(jié)內(nèi)存對齊算法有兩種:
alloc源碼分析中的align16
malloc源碼分析中的segregated_size_to_fit
#define SHIFT_NANO_QUANTUM 4
#define NANO_REGIME_QUANTA_SIZE (1 << SHIFT_NANO_QUANTUM) // 16
static MALLOC_INLINE size_t
segregated_size_to_fit(nanozone_t *nanozone, size_t size, size_t *pKey)
{
size_t k, slot_bytes;
if (0 == size) {
size = NANO_REGIME_QUANTA_SIZE; // Historical behavior
}
k = (size + NANO_REGIME_QUANTA_SIZE - 1) >> SHIFT_NANO_QUANTUM; // round up and shift for number of quanta
slot_bytes = k << SHIFT_NANO_QUANTUM; // multiply by power of two quanta size
*pKey = k - 1; // Zero-based!
return slot_bytes;
}
-
NANO_REGIME_QUANTA_SIZE
表示的是1左移4位辆影,得到16。
//表示1
0000 0000 0000 0001
//左移4位
0000 0000 0001 0000 = 16
-
size + NANO_REGIME_QUANTA_SIZE - 1
黍特,假如size
等于40蛙讥,得出的結(jié)果為55 -
>> SHIFT_NANO_QUANTUM
表示左移4位,
//55的二進制
0000 0000 0011 0111
//左移4位
0000 0000 0000 0011
- 然后灭衷,
k << SHIFT_NANO_QUANTUM
次慢,表示右移4位
//k的二進制
0000 0000 0000 0011
//右移4位
0000 0000 0011 0000
可以看到,前面4位都抹0翔曲,所以可以得出結(jié)論迫像,系統(tǒng)進行內(nèi)存分配的時候,進行了16位的內(nèi)存對齊瞳遍。
結(jié)論
雖然我們的結(jié)構(gòu)體遵循內(nèi)存對齊的原則闻妓,但是,它不會隨意的浪費內(nèi)存掠械,會通過內(nèi)存重排的方式進行結(jié)構(gòu)優(yōu)化由缆,將多余的內(nèi)存進行合理的利用,盡最大的可能節(jié)省內(nèi)存份蝴。