iOS 內(nèi)存對(duì)齊

一、結(jié)構(gòu)體內(nèi)存對(duì)齊

1.1 結(jié)構(gòu)體內(nèi)存對(duì)齊三大原則

  • 數(shù)據(jù)成員對(duì)?規(guī)則
    結(jié)構(gòu)體(struct)或聯(lián)合體(union)的數(shù)據(jù)成員,第一個(gè)數(shù)據(jù)成員放在offset0的地方,以后每個(gè)數(shù)據(jù)成員存儲(chǔ)的起始位置要從該成員大小或者成員的子成員大小(只要該成員有子成員,比如說是數(shù)組,結(jié)構(gòu)體等)的整數(shù)倍開始。(比如int4字節(jié),則要從4的整數(shù)倍地址開始存儲(chǔ)。)
    min(當(dāng)前開始的位置m n) m=9 n=4 9 10 11 12

  • 結(jié)構(gòu)體作為成員
    如果一個(gè)結(jié)構(gòu)里有某些結(jié)構(gòu)體成員,則結(jié)構(gòu)體成員要從其內(nèi)部最大元素大小的整數(shù)倍地址開始存儲(chǔ)。
    比如struct a中存有struct b,bchar,int ,double等元素。那b應(yīng)該從8的整數(shù)倍開始存儲(chǔ)。

struct structB {
    char c;
    int i;
    double d;
};

struct structA {
    char c;//1
    int i;//4
    struct structB b;//從8開始存儲(chǔ)
};
  • 補(bǔ)齊
    結(jié)構(gòu)體的總大小,也就是sizeof的結(jié)果,必須是其內(nèi)部最大成員的整數(shù)倍。不足的要補(bǔ)?。

1.2 基本數(shù)據(jù)類型內(nèi)存大小

C OC 32位 64位
bool BOOL(64位) 1 1
signed char (__signed char)int8_t、BOOL(32位) 1 1
unsigned char Boolean 1 1
short int16_t 2 2
unsigned short unichar 2 2
int 、int32_t NSInteger(32位)、boolean_t(32位) 4 4
unsigned int boolean_t(64位)、NSUInteger(32位) 4 4
long NSInteger(64位) 4 8
unsigned long NSUInteger(64位) 4 8
long long int64_t 8 8
float CGFloat(32位) 4 4
double CGFloat(64位) 8 8

1.3 獲取內(nèi)存大小

1.3.1 sizeof(expression-or-type)

sizeof()C/C++中的關(guān)鍵字,它是一個(gè)運(yùn)算符,不是函數(shù)。作用是取得一個(gè)對(duì)象(數(shù)據(jù)類型或者數(shù)據(jù)對(duì)象)的長(zhǎng)度(即占用內(nèi)存的大小,以byte為單位,返回size_t)?;緮?shù)據(jù)類型(int、double等)的大小與系統(tǒng)相關(guān)。結(jié)構(gòu)體涉及字節(jié)對(duì)齊。
示例:

struct Stu {
    char c;
    int i;
    double d;
};

void test() {
    //基本數(shù)據(jù)類型
    int age = 18;
    size_t sizeAge1 = sizeof(age);
    size_t sizeAge2 = sizeof age;
    size_t sizeAge3 = sizeof(int);
    NSLog(@"age size: %zu, %zu, %zu",sizeAge1,sizeAge2,sizeAge3);

    //結(jié)構(gòu)體
    struct Stu s;
    s.c = 'c';
    s.i = 18;
    s.d = 180.0;
    
    size_t sizeS1 = sizeof(s);
    size_t sizeS2 = sizeof s;
    size_t sizeS3 = sizeof(struct Stu);
    NSLog(@"s size: %zu, %zu, %zu",sizeS1,sizeS2,sizeS3);

    //指針
    NSObject *obj = [NSObject alloc];
    size_t sizeObj1 = sizeof(obj);
    size_t sizeObj2 = sizeof obj;
    size_t sizeObj3 = sizeof(NSObject *);
    NSLog(@"obj size: %zu, %zu, %zu",sizeObj1,sizeObj2,sizeObj3);
}

輸出:

age size: 4, 4, 4
s size: 16, 16, 16
obj size: 8, 8, 8
  • 通過類型和實(shí)例都可以獲取內(nèi)存大小,這也說明開辟的內(nèi)存大小在類型確定后就已經(jīng)確定了。
  • sizeof是運(yùn)算符不是函數(shù)。3種語法形式都可以,需要注意的是通過類型獲取的方式必須在()中。

1.3.2 class_getInstanceSize

這個(gè)函數(shù)是runtime提供的獲取類的實(shí)例所占用的內(nèi)存大小。大小只與成員變量有關(guān)。獲取的是實(shí)際占用的空間(8字節(jié)對(duì)齊)。
源碼如下:

#ifdef __LP64__
#   define WORD_MASK 7UL
#else
#   define WORD_MASK 3UL
#endif

size_t class_getInstanceSize(Class cls)
{
    if (!cls) return 0;
    return cls->alignedInstanceSize();
}

// Class's ivar size rounded up to a pointer-size boundary.
uint32_t alignedInstanceSize() const {
    return word_align(unalignedInstanceSize());
}

static inline uint32_t word_align(uint32_t x) {
    return (x + WORD_MASK) & ~WORD_MASK;
}

uint32_t unalignedInstanceSize() const {
    ASSERT(isRealized());
    return data()->ro()->instanceSize;
}

這里就和allocinstanceSize沒有命中緩存的邏輯一致了。

1.3.3 malloc_size

malloc_size就是alloc中實(shí)際開辟的空間。

1.4 結(jié)構(gòu)體對(duì)齊案例

1. 有如下結(jié)構(gòu)體Struct1Struct2分別占用多大內(nèi)存?

struct Struct1 {
    double a; // [0,7]
    char b;     // [8]
    int c;        // 根據(jù)第一準(zhǔn)則要從4的倍數(shù)開始,所以[12,13,14,15]。跳過9,10,11
    short d;  //[16,17]
}struct1;
//根據(jù)第三準(zhǔn)則總大小要是8的倍數(shù),那就要分配24字節(jié)。

struct Struct2 {
    double a; //[0,7]
    int b;        //[8,11]
    char c;     //[12]
    short d;   //根據(jù)準(zhǔn)則1跳過13,從14開始 [14,15]
}struct2;
//這里0~15大小本來就為16了,所以不需要補(bǔ)齊了。

驗(yàn)證:

NSLog(@"struct1 size :%zu\nstruct2 size:%zu",sizeof(struct1),sizeof(struct2));

輸出:

struct1 size :24
struct2 size:16
  • 結(jié)構(gòu)體中數(shù)據(jù)類型順序不一致占用的內(nèi)存大小可能不一致。
  • 大小計(jì)算從0開始,Struct2并沒有進(jìn)行第三原則補(bǔ)齊。

2. 增加一個(gè)Struct3中有結(jié)構(gòu)體嵌套,那么占用大小是多少?

struct Struct3 {
    double a;                  //[0,7]
    int b;                         //[8,11]
    char c;                     //[12]
    short d;                   //跳過13 [14,15]
    int e;                        // [16,19]
    struct Struct1 str; //根據(jù)準(zhǔn)則2,Struct1最大元素為`double`類型,所以從24開始。根據(jù)`Struct1`分配的時(shí)候24個(gè)字節(jié),所以str為[24,47]
}struct3;
//所以Struct3占用內(nèi)存大小為48字節(jié)。

驗(yàn)證:

NSLog(@"struct3 size :%zu",sizeof(struct3));

struct3 size :48

在這里可能有個(gè)疑問準(zhǔn)則3是先作用在Struct1再作用在Struct3還是最后直接作用在Struct3?不防驗(yàn)證一下:

struct Struct4 {
    struct Struct1 str;
    char c;
}struct4;

Struct1本身占用18字節(jié),補(bǔ)齊后占用24字節(jié)。如果Struct4最終占用32字節(jié)那么就是第一種情況,占用24字節(jié)則是第二種情況。

NSLog(@"struct4 size :%zu",sizeof(struct4));
struct4 size :32

這也就驗(yàn)證了猜想,結(jié)構(gòu)體嵌套從內(nèi)部開始補(bǔ)齊。這也符合常理。

3. 修改結(jié)構(gòu)體如下:

struct S1 {
    int a; // 4  [0,3]
    char b;// 1  [4]
    short c; // 2 [6,7]
}; // 0~7 8字節(jié)

struct S2 {
    double a; // 8 [0,7]
    char b;   // 1 [8]
    struct S1 s1; // 8  [12,19]  按s1自身中存的最大a的4字節(jié)的倍數(shù)對(duì)齊
    bool c; // 1 [20]
};
//0~20一共21個(gè)字節(jié),按最大的8字節(jié)對(duì)齊。應(yīng)該24字節(jié)。
struct S2  s2;
NSLog(@"size :%zu",sizeof(s2));

這個(gè)時(shí)候s2大小為多少?

分析:

  • S1:
    • int a4個(gè)字節(jié),從[0~3]。
    • char b1個(gè)字節(jié),[4]。
    • short c2個(gè)字節(jié),需要以2字節(jié)對(duì)齊,所以跳過5 [6~7]
    • S1整體從0~7不需要補(bǔ)齊。占8字節(jié)。
  • S2:
    • double a8字節(jié),[0~7]。
    • char b1字節(jié),[8]。
    • struct S1 s18字節(jié)。由于S1內(nèi)部最大元素為int a所以需要4倍對(duì)齊,所以[12~19]。
    • bool c1字節(jié),[20]。
    • S2整體從0~21一共21字節(jié),需要按S2中最大元素double a補(bǔ)齊。所以應(yīng)該是24字節(jié)。

輸出:

size :24

1.5 對(duì)齊原理分析

為什么要根據(jù)數(shù)據(jù)類型跳過部分內(nèi)存呢?跳過的部分為什么不能存儲(chǔ)數(shù)據(jù)?


內(nèi)存對(duì)齊對(duì)比示意圖

對(duì)于不優(yōu)化聯(lián)系存儲(chǔ)的情況,CPU讀取8~15的內(nèi)存數(shù)據(jù),需要先讀取1字節(jié)再讀取4字節(jié),cpu對(duì)于要讀取的數(shù)據(jù)大小是有變化的。而優(yōu)化后cpu先讀取4字節(jié)(由于白色3字節(jié)空白所以可以直接讀取4字節(jié))再讀取4字節(jié)在這段內(nèi)存中是沒有變化的。相比于第一種優(yōu)化后cpu的要進(jìn)行的操作少了,這就實(shí)現(xiàn)了通過空間換取時(shí)間。

二、系統(tǒng)內(nèi)存開辟

2.1 案例

HPObject定義如下:

@interface HPObject : NSObject

@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *nickName;
@property (nonatomic, assign) int age;
@property (nonatomic, assign) long height;

@end

調(diào)用:

#import "HPObject.h"
#import <objc/runtime.h>
#import <malloc/malloc.h>

HPObject *hpObj = [HPObject alloc];
hpObj.name = @"HotpotCat";
hpObj.age = 18;

NSLog(@"sizeof:%zu class_getInstanceSize:%zu malloc:%zu",sizeof(hpObj),class_getInstanceSize([HPObject class]),malloc_size((__bridge const void *)(hpObj)));

那么sizeofclass_getInstanceSize、malloc_size分別輸出多少呢?
驗(yàn)證:

sizeof:8 class_getInstanceSize:40 malloc:48
  • hpObj是一個(gè)結(jié)構(gòu)體指針sizeof返回8。
  • class_getInstanceSize由于存在isa8字節(jié)對(duì)齊所以返回40
  • malloc_size為什么返回48呢?

2.2 malloc_size分析

查看API發(fā)現(xiàn)malloc_sizeusr/include中:

image.png

那么就需要源碼查看calloc的實(shí)現(xiàn)邏輯了。
calloc源碼在libmalloc中。
在源碼調(diào)試中實(shí)現(xiàn)如下:

void *p = calloc(1, 40);
NSLog(@"%zu",malloc_size(p));

calloc

void *
calloc(size_t num_items, size_t size)
{
    return _malloc_zone_calloc(default_zone, num_items, size, MZ_POSIX);
}

calloc調(diào)用了_malloc_zone_calloc

_malloc_zone_calloc核心實(shí)現(xiàn)如下

MALLOC_NOINLINE
static void *
_malloc_zone_calloc(malloc_zone_t *zone, size_t num_items, size_t size,
        malloc_zone_options_t mzo)
{
//……
    void *ptr;
//……
    ptr = zone->calloc(zone, num_items, size);
//……
    return ptr;
}

內(nèi)部調(diào)用了zone->calloc。但是只有calloc的聲明沒有實(shí)現(xiàn)。這個(gè)時(shí)候有兩種方式去找到調(diào)用:

  • control + step into跟進(jìn)去看跳轉(zhuǎn)到了哪里。
  • 直接po/ p zone->calloc查看(有賦值就有存儲(chǔ)值)
(lldb) po zone->calloc
(.dylib`default_zone_calloc at malloc.c:385)
(lldb) p zone->calloc
(void *(*)(_malloc_zone_t *, size_t, size_t)) $0 = 0x00000001002e1bb7 (.dylib`default_zone_calloc at malloc.c:385)

default_zone_calloc

static void *
default_zone_calloc(malloc_zone_t *zone, size_t num_items, size_t size)
{
    zone = runtime_default_zone();
    
    return zone->calloc(zone, num_items, size);
}

同理這里的zone->calloc調(diào)用到了nano_calloc

nano_calloc

static void *
nano_calloc(nanozone_t *nanozone, size_t num_items, size_t size)
{
    size_t total_bytes;

    //NULL邏輯不用看
    if (calloc_get_size(num_items, size, 0, &total_bytes)) {
        return NULL;
    }

    if (total_bytes <= NANO_MAX_SIZE) {
        //重點(diǎn)邏輯
        void *p = _nano_malloc_check_clear(nanozone, total_bytes, 1);
        if (p) {
            return p;
        } else {
            /* FALLTHROUGH to helper zone */
        }
    }
    //help邏輯大概出錯(cuò)的時(shí)候才需要
    malloc_zone_t *zone = (malloc_zone_t *)(nanozone->helper_zone);
    return zone->calloc(zone, 1, total_bytes);
}

得到核心邏輯在_nano_malloc_check_clear中。

_nano_malloc_check_clear

image.png

_nano_malloc_check_clear中計(jì)算大小的邏輯是在segregated_size_to_fit中完成的。

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) {
        //當(dāng)size為0的時(shí)候直接返回16
        size = NANO_REGIME_QUANTA_SIZE; // Historical behavior
    }
    //(40 + 16 - 1) >> 4 << 4  16字節(jié)對(duì)齊
    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;
}

16字節(jié)對(duì)齊,向上對(duì)齊。這也就是在系統(tǒng)的內(nèi)存堆區(qū)中對(duì)象的內(nèi)存是16字節(jié)對(duì)齊,成員變量是以8字節(jié)對(duì)齊(結(jié)構(gòu)體內(nèi)部)。對(duì)象與對(duì)象是16字節(jié)對(duì)齊。

calloc調(diào)用流程

2.3 為什么以16字節(jié)對(duì)齊?

為什么對(duì)象不以8字節(jié)對(duì)齊?而以16字節(jié)對(duì)齊?

對(duì)象對(duì)齊對(duì)比

假如一個(gè)對(duì)象內(nèi)部成員變量都是8字節(jié)大小。

  • 8字節(jié)對(duì)齊內(nèi)部沒有多余空間,更容易發(fā)生訪問錯(cuò)誤。
  • 16字節(jié)對(duì)齊內(nèi)部有多余空間,不容易發(fā)生訪問錯(cuò)誤。

對(duì)于64字節(jié)的空間:

16 32 48 64
8 16 24 32 40 48 56 64
  • 明顯以16字節(jié)對(duì)齊訪問對(duì)象和成員變量碰到一起的概率小了。16字節(jié)對(duì)齊4次,8字節(jié)對(duì)齊8次。
  • 任何對(duì)象都繼承自NSObject,但是很少有對(duì)象只有一個(gè)isa。所以最小的對(duì)象都應(yīng)該是16
  • 如果用32字節(jié)對(duì)齊呢?很明顯空間浪費(fèi)太大。

三、getInstanceSize與calloc

getInstanceSize的邏輯可以查看上一篇文章alloc流程。getInstanceSizecalloc中字節(jié)對(duì)齊關(guān)系如下:

getInstanceSize&calloc內(nèi)存對(duì)齊

  1. instanceSize 正常情況下會(huì)走fastInstanceSize分支進(jìn)行16字節(jié)對(duì)齊。(setFastInstanceSize_read_images的時(shí)候就完成了進(jìn)行了8字節(jié)對(duì)齊邏輯,但是沒有進(jìn)行最小16字節(jié)兜底)。
  2. instanceSize alignedInstanceSize分支進(jìn)行了8字節(jié)對(duì)齊并且有最小16字節(jié)兜底。但是沒有進(jìn)行16字節(jié)對(duì)齊。
  3. calloc中進(jìn)行了16字節(jié)對(duì)齊和最小16字節(jié)的修正。相當(dāng)于是對(duì)instanceSize的兩個(gè)分支的兜底。

成員變量字節(jié)對(duì)齊是8字節(jié)對(duì)齊,對(duì)象的內(nèi)存對(duì)齊是16字節(jié)對(duì)齊

總結(jié)

  • 結(jié)構(gòu)體對(duì)齊(三個(gè)原則)
    • 3個(gè)原則
      • 數(shù)據(jù)成員對(duì)?規(guī)則:從成員大小或者成員的子成員大小整數(shù)倍開始。
      • 結(jié)構(gòu)體作為成員:從內(nèi)部成員最大元素大小的整數(shù)倍地址開始存儲(chǔ)。
      • 補(bǔ)齊:必須是內(nèi)部最大成員的整數(shù)倍,不足的要補(bǔ)?。
    • 對(duì)齊原理:優(yōu)化CPU讀取速度,以空間換時(shí)間。
    • 結(jié)構(gòu)體嵌套補(bǔ)齊從內(nèi)部開始補(bǔ)齊。
  • 內(nèi)存大小獲取
    • sizeof:是運(yùn)算符,不是函數(shù)。獲取對(duì)象的長(zhǎng)度(對(duì)象本身)。
    • class_getInstanceSize:獲取類的實(shí)例所占用的內(nèi)存大小。大小只與成員變量有關(guān)。
    • malloc_sizealloc中實(shí)際開辟的空間。
  • calloc 16字節(jié)對(duì)齊,最小返回16。
    • 最終分配的內(nèi)存大小邏輯在segregated_size_to_fit中。以16字節(jié)對(duì)齊向上取整
    • 為什么以16字節(jié)對(duì)齊?減少訪問錯(cuò)誤。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • iOS 內(nèi)存對(duì)其原則 數(shù)據(jù)成員對(duì)?規(guī)則:結(jié)構(gòu)(struct)(或聯(lián)合(union))的數(shù)據(jù)成員,第?個(gè)數(shù)據(jù)成員放在...
    Gumball_a45f閱讀 302評(píng)論 0 0
  • 前言 在iOS底層源碼學(xué)習(xí)中,會(huì)需要分析一個(gè)結(jié)構(gòu)體所占用的內(nèi)存大小,這里面就涉及到了內(nèi)存對(duì)齊 今天,我將結(jié)合內(nèi)存對(duì)...
    002and001閱讀 1,900評(píng)論 0 5
  • 首先我們先看一下內(nèi)存對(duì)齊原則: 1.數(shù)據(jù)成員對(duì)?規(guī)則:結(jié)構(gòu)(struct)(或聯(lián)合(union))的數(shù)據(jù)成員,第一...
    e521閱讀 490評(píng)論 0 0
  • 這篇文章我們來探索一下iOS內(nèi)存對(duì)齊的原理,在探索完內(nèi)存對(duì)齊原理之后,你就會(huì)明白內(nèi)存對(duì)齊的好處。 在講述內(nèi)存對(duì)齊時(shí)...
    大橘豬豬俠閱讀 1,045評(píng)論 0 3
  • 影響OC對(duì)象內(nèi)存大小的因素 數(shù)據(jù)類型內(nèi)存大?。?代碼分析 通過class_getInstanceSize獲取實(shí)例的...
    qinghan閱讀 352評(píng)論 1 1

友情鏈接更多精彩內(nèi)容