iOS底層原理--內(nèi)存對齊

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é)果如下:

打印結(jié)構(gòu)體.png

明顯看到,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)存大小如圖:


各數(shù)據(jù)類型所占內(nèi)存.png

所以按照規(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

最終得出了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拗慨、KCCooci奉芦。但是77309436513這一串是個亂碼

  • 我們把0x0000001200006261拆分成0x00000012,0x62,0x61,分別得到

    image.png

    可以得到18,a,b了赵抢。

通過ASCII換算得出97為a,98為b。

我們用一張圖來表示


LGPerson.png

對象的內(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é)果如下:


LGPerson.png

對打印信息進行分析

  • 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)存份蝴。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末犁功,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子婚夫,更是在濱河造成了極大的恐慌浸卦,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,273評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件案糙,死亡現(xiàn)場離奇詭異限嫌,居然都是意外死亡靴庆,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,349評論 3 398
  • 文/潘曉璐 我一進店門怒医,熙熙樓的掌柜王于貴愁眉苦臉地迎上來炉抒,“玉大人,你說我怎么就攤上這事稚叹⊙姹。” “怎么了?”我有些...
    開封第一講書人閱讀 167,709評論 0 360
  • 文/不壞的土叔 我叫張陵扒袖,是天一觀的道長塞茅。 經(jīng)常有香客問我,道長季率,這世上最難降的妖魔是什么野瘦? 我笑而不...
    開封第一講書人閱讀 59,520評論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮飒泻,結(jié)果婚禮上鞭光,老公的妹妹穿的比我還像新娘。我一直安慰自己泞遗,他們只是感情好惰许,可當我...
    茶點故事閱讀 68,515評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著史辙,像睡著了一般啡省。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上髓霞,一...
    開封第一講書人閱讀 52,158評論 1 308
  • 那天,我揣著相機與錄音畦戒,去河邊找鬼方库。 笑死障斋,一個胖子當著我的面吹牛垃环,可吹牛的內(nèi)容都是我干的寥院。 我是一名探鬼主播秸谢,決...
    沈念sama閱讀 40,755評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了冷尉?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,660評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體侵佃,經(jīng)...
    沈念sama閱讀 46,203評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡迈螟,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,287評論 3 340
  • 正文 我和宋清朗相戀三年季春,在試婚紗的時候發(fā)現(xiàn)自己被綠了蚕脏。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片秦驯。...
    茶點故事閱讀 40,427評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡洛心,死狀恐怖厅目,靈堂內(nèi)的尸體忽然破棺而出深啤,到底是詐尸還是另有隱情诱桂,我是刑警寧澤,帶...
    沈念sama閱讀 36,122評論 5 349
  • 正文 年R本政府宣布哀峻,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏犬缨。R本人自食惡果不足惜枝恋,卻給世界環(huán)境...
    茶點故事閱讀 41,801評論 3 333
  • 文/蒙蒙 一嗡害、第九天 我趴在偏房一處隱蔽的房頂上張望焚碌。 院中可真熱鬧,春花似錦霸妹、人聲如沸十电。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,272評論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽鹃骂。三九已至,卻和暖如春首妖,著一層夾襖步出監(jiān)牢的瞬間偎漫,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,393評論 1 272
  • 我被黑心中介騙來泰國打工有缆, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留象踊,地道東北人。 一個月前我還...
    沈念sama閱讀 48,808評論 3 376
  • 正文 我出身青樓棚壁,卻偏偏與公主長得像杯矩,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子袖外,可洞房花燭夜當晚...
    茶點故事閱讀 45,440評論 2 359