OC對(duì)象原理探索(中)-內(nèi)存對(duì)齊

christopher-gower-m_HRfLhgABo-unsplash.jpg

什么是內(nèi)存對(duì)齊

關(guān)于什么是內(nèi)存對(duì)齊,我們通過下面一個(gè)例子來看一下

需引入#import <objc/runtime.h> #import <malloc/malloc.h>

    //LMPerson 有兩個(gè)成員變量:name gendar
    LMPerson *p = [LMPerson alloc];
    p.name = @"LM";
    p.gendar = @"男";
    NSLog(@"person---sizeof-------------%lu",sizeof(p));
    NSLog(@"person---instanceSize-------%lu",class_getInstanceSize([LMPerson class]));
    NSLog(@"person---malloc_size--------%lu",malloc_size((__bridge const void *)p));
    //LMStudent 有三個(gè)成員變量 name gendar isAdult
    LMStudent *student = [LMStudent alloc];
    student.name = @"小明";
    student.gendar = @"男";
    student.isAdult = YES;
    NSLog(@"student---sizeof-------------%lu",sizeof(student));
    NSLog(@"student---instanceSize-------%lu",class_getInstanceSize([LMStudent class]));
    NSLog(@"student---malloc_size--------%lu",malloc_size((__bridge const void *)student));

打印信息:

person---sizeof-------------8
person---instanceSize-------24
person---malloc_size--------32
student---sizeof-------------8
student---instanceSize-------32
student---malloc_size--------32

為了更好的看懂打印的結(jié)果,我們先了解下sizeof,class_getInstanceSize,malloc_size

  • sizeof是C/C++中的關(guān)鍵字,它是一個(gè)運(yùn)算符,其作用是取得一個(gè)對(duì)象(數(shù)據(jù)類型或者數(shù)據(jù)對(duì)象)的長度(即占用內(nèi)存的大小,以byte為單位)。
  • class_getInstanceSize計(jì)算對(duì)象及成員變量占用的內(nèi)存空間
  • malloc_size 系統(tǒng)實(shí)際分配的內(nèi)存大小
    基礎(chǔ)數(shù)據(jù)類型占用空間大?。?br>
    基礎(chǔ)數(shù)據(jù)類型占用空間大小

我們來分析下打印的結(jié)果:

  1. 類的本質(zhì)是一個(gè)結(jié)構(gòu)體,這里的pstudent就是一個(gè)結(jié)構(gòu)體指針,結(jié)構(gòu)體指針的大小是8字節(jié),所以sizeof打印都為8
  2. LMPerson 內(nèi)有兩個(gè)成員變量 name gendar 都是NSString類型,NSString類型占用8字節(jié)。
    LMStudent內(nèi)有三個(gè)成員變量name gendar isAdult,name gendar 為``NSString類型各占8字節(jié),isAdultBOOL類型占1字節(jié)。
    每個(gè)類里,蘋果都設(shè)計(jì)包含一個(gè)isa,查看源碼
typedef struct objc_class *Class;

/// Represents an instance of a class.
struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};

發(fā)現(xiàn)isa是一個(gè)結(jié)構(gòu)體指針,占用8個(gè)字節(jié)
對(duì)比我們發(fā)現(xiàn)LMStudentLMPerson多一個(gè)BOOL類型的成員變量,內(nèi)存居然相差了8個(gè)字節(jié),說明內(nèi)部進(jìn)行了內(nèi)存對(duì)齊,具體是不是這樣,我們后面分析class_getInstanceSize探究結(jié)果
3.p實(shí)際的內(nèi)存地址是24,為什么系統(tǒng)分配的內(nèi)存是32呢? 這里malloc_size也進(jìn)行了內(nèi)存對(duì)齊對(duì)齊方式可以看出是16位對(duì)齊,具體是不是這樣,我們后面分析malloc_size探究結(jié)果

在C/C++中,結(jié)構(gòu)是一種復(fù)合數(shù)據(jù)類型,其構(gòu)成元素既可以是基本數(shù)據(jù)類型(如int、long、float等)的變量,也可以是一些復(fù)合數(shù)據(jù)類型(如數(shù)組、結(jié)構(gòu)、聯(lián)合等)的數(shù)據(jù)單元。元素是按照定義順序一個(gè)一個(gè)放到內(nèi)存中去的,但并不是緊密排列的。這就是 內(nèi)存對(duì)齊

為什么要內(nèi)存對(duì)齊

1.平臺(tái)原因(移植原因):不是所有的硬件平臺(tái)都能訪問任意地址上的任意數(shù)據(jù)的;某些硬件平臺(tái)只能在某些地址處取某些特定類型的數(shù)據(jù),否則拋出硬件異常。
2.性能原因:數(shù)據(jù)結(jié)構(gòu)(尤其是棧)應(yīng)該盡可能地在自然邊界上對(duì)齊。原因在于,為了訪問未對(duì)齊的內(nèi)存,處理器需要作兩次內(nèi)存訪問;而對(duì)齊的內(nèi)存訪問僅需要一次訪問。

內(nèi)存對(duì)齊的規(guī)則

  1. 數(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ǔ))。
  2. 結(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ǔ).)
  3. 結(jié)構(gòu)體的總大小,也就是sizeof的結(jié)果,.必須是其內(nèi)部最大成員的整數(shù)倍.不足的要補(bǔ)?。

下面我們通過一個(gè)例子來分析結(jié)構(gòu)體占用的實(shí)際內(nèi)存大小

struct LGStruct1 {
    double a;       // 8     double 類型在64位上占8個(gè)字節(jié),位置為[0 7]
    char b;         // 1     char 類型在64位上占1個(gè)字節(jié) 第8位是1的倍數(shù),所以在[8]
    int c;          // 4     int 類型在64位上占4個(gè)字節(jié),下面第9,10,11位都不是4的倍數(shù),所以跳過從12位開始存放(9 10 11 [12 13 14 15]
    short d;        // 2     short類型在64位上占2個(gè)字節(jié),第16位是2的倍數(shù),所以從16位開始存放位置為 [16 17],結(jié)構(gòu)體內(nèi)最大的占8個(gè)字節(jié),結(jié)構(gòu)體總大小要為8的倍數(shù),所以結(jié)構(gòu)體的總大小為 24
}struct1;

struct LGStruct2 {
    double a;       // 8    double 類型在64位上占8個(gè)字節(jié),位置為[0 7]
    int b;          // 4    int 類型在64位上占4個(gè)字節(jié),第8位是4的倍數(shù),從第8位開始存放,位置為[8 9 10 11]
    char c;         // 1    char 類型在64位上占1個(gè)字節(jié) 第8位是1的倍數(shù),所以位置為[12]
    short d;        // 2     short類型在64位上占2個(gè)字節(jié),第13位不是2的倍數(shù),所以從第14位開始存放(13 [14 15],結(jié)構(gòu)體內(nèi)最大的占8個(gè)字節(jié),結(jié)構(gòu)體總大小要為8的倍數(shù),所以結(jié)構(gòu)體的總大小為 16
}struct2;
struct LGStruct3 {
    double a;      // 8    double 類型在64位上占8個(gè)字節(jié),位置為[0 7]
    int b;            // 4    int 類型在64位上占4個(gè)字節(jié),第8位是4的倍數(shù),從第8位開始存放,位置為[8 9 10 11]
    char c;         // 1    char 類型在64位上占1個(gè)字節(jié) 第8位是1的倍數(shù),所以位置為[12]
    short d;       // 2     short類型在64位上占2個(gè)字節(jié),第13位不是2的倍數(shù),所以從第14位開始存放(13 [14 15]
    int e;           //4     int 類型在64位上占4個(gè)字節(jié),第16位是4的倍數(shù),所以從第16位開始存放[16 17 18 19]
    struct LGStruct1 str;    //24   上面已經(jīng)分析 LGStruct1 實(shí)際占用24,  第20位不是24的倍數(shù),所以 從 24位開始 (20 21 22 23 [24...47]  結(jié)構(gòu)體str內(nèi)最大的占8個(gè)字節(jié),結(jié)構(gòu)體總大小要為8的倍數(shù),所以結(jié)構(gòu)體的總大小為 48
}struct3;
struct LGStruct4 {
    float a;   //4   [0 1 2 3]
    double b;  //8   (4 5 6 7 [8 9 10 11 12 13 14 15]
    int c;     //4   [16 17 18 19]
    char d;    //1   [20]
    struct LGStruct1 str1; // 24 (21 22 23 [24....47]
    struct LGStruct2 str2;  //16 [48 ...63]  64
}struct4;

打印驗(yàn)證一下:

NSLog(@"%lu-%lu-%lu",sizeof(struct1),sizeof(struct2),sizeof(struct3));
24-16-48

趁熱打鐵再分析幾個(gè):

struct LGStruct5 {
    float a;   //4   [0 1 2 3]
    double b;  //8   (4 5 6 7 [8 9 10 11 12 13 14 15]
    int c;     //4   [16 17 18 19]
    char d;    //1   [20]
    struct LGStruct2 str2;  //16 (21 ...[24.. 39]
    struct LGStruct1 str1;  //24 [40...63]   64
}struct5;
struct LGStruct6 {
    float a;   //4   [0 1 2 3]
    double b;  //8   (4 5 6 7 [8 9 10 11 12 13 14 15]
    int c;     //4   [16 17 18 19]
    char d;    //1   [20]
    struct LGStruct2 str2;  //16 (21 ...[24.. 39]
    short e;   //2   [40 41]
    struct LGStruct1 str1;  //24 [48...71]   72
}struct6;

class_getInstanceSize 源碼分析

我們分下class_getInstanceSize源碼確認(rèn)是做了8字節(jié)對(duì)齊

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

走到alignedInstanceSize方法內(nèi)

    uint32_t alignedInstanceSize() const {
        return word_align(unalignedInstanceSize());
    }

static inline size_t word_align(size_t x) {
    return (x + WORD_MASK) & ~WORD_MASK;
}
//64位下
#   define WORD_MASK 7UL
比如 x=4 
4+7=13
0000 1101
7
0000 0111
~7
1111 1000
13 & ~7   0000 1101 & 1111 1000
= 0000 1000  即8

所以最終 class_getInstanceSize 以8字節(jié)對(duì)齊

malloc_size 源碼分析

首先我們點(diǎn)擊 malloc_size 發(fā)現(xiàn)源碼在libmalloc,編譯:

malloc_size

#import <Foundation/Foundation.h>
#import <malloc/malloc.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        //申請(qǐng)開辟40字節(jié)內(nèi)存,系統(tǒng)最終開辟48字節(jié)
        void *p = calloc(1, 40);
        NSLog(@"系統(tǒng)分配的內(nèi)存空間---%lu",malloc_size(p));
        //打印//系統(tǒng)分配的內(nèi)存空間---48
    }
    return 0;
}

malloc_size是獲取系統(tǒng)分配的內(nèi)存大小 calloc是系統(tǒng)開辟內(nèi)存,所以malloc_size底層流程其實(shí)是calloc
所以我們探究下calloc都做了什么

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

static void *
_malloc_zone_calloc(malloc_zone_t *zone, size_t num_items, size_t size,
        malloc_zone_options_t mzo)
{
    MALLOC_TRACE(TRACE_calloc | DBG_FUNC_START, (uintptr_t)zone, num_items, size, 0);

    void *ptr;
    if (malloc_check_start) {
        internal_check();
    }
//我們看到 return ptr;所以斷定下面這行是關(guān)鍵代碼
    ptr = zone->calloc(zone, num_items, size);

    if (os_unlikely(malloc_logger)) {
        malloc_logger(MALLOC_LOG_TYPE_ALLOCATE | MALLOC_LOG_TYPE_HAS_ZONE | MALLOC_LOG_TYPE_CLEARED, (uintptr_t)zone,
                (uintptr_t)(num_items * size), 0, (uintptr_t)ptr, 0);
    }

    MALLOC_TRACE(TRACE_calloc | DBG_FUNC_END, (uintptr_t)zone, num_items, size, (uintptr_t)ptr);
    if (os_unlikely(ptr == NULL)) {
        malloc_set_errno_fast(mzo, ENOMEM);
    }
    return ptr;
}

到這里我們發(fā)現(xiàn)zone->calloc點(diǎn)不進(jìn)去了,怎么辦呢,我們可以采用之前查看匯編的方法:打斷點(diǎn)然后Debug->Debug Workflow -> Always show Disassembly:

斷點(diǎn)

zone->calloc所在位置

我們也可以使用LLDB打印的方法:
zone->calloc所在位置

我們接著看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);
}

這里同樣可以通過匯編或者打印看下一步執(zhí)行的步驟:

default_zone_calloc

nano_calloc

_nano_malloc_check_clear

因?yàn)槲覀冎魂P(guān)心內(nèi)存的變化,這里注意size_t 有一個(gè)segregated_size_to_fit操作。segregated_size_to_fit做了什么呢?我們接著往下看:

#define SHIFT_NANO_QUANTUM      4
#define NANO_REGIME_QUANTA_SIZE (1 << SHIFT_NANO_QUANTUM)   // 16

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  16
    }
    k = (size + NANO_REGIME_QUANTA_SIZE - 1) >> SHIFT_NANO_QUANTUM; // round up and shift for number of quanta
    //k = (40 + 16 -1) >> 4
    slot_bytes = k << SHIFT_NANO_QUANTUM;                           // multiply by power of two quanta size
    //slot_bytes = (40 + 16 -1) >> 4 << 4
    *pKey = k - 1;                                                  // Zero-based!

    return slot_bytes;
}

經(jīng)過對(duì)齊算法,按照16字節(jié)對(duì)齊之后40字節(jié)按48字節(jié)開辟空間

總結(jié)

內(nèi)存對(duì)齊規(guī)則

  • 數(shù)據(jù)成員對(duì)齊規(guī)則:從該成員大小或者成員的子成員大小整數(shù)倍開始。
  • 結(jié)構(gòu)體作為成員對(duì)齊規(guī)則:結(jié)構(gòu)體成員要從 其內(nèi)部最大元素大小的整數(shù)倍地址開始存儲(chǔ)。
  • 結(jié)構(gòu)體的總大小,也就是sizeof的結(jié)果,.必須是其內(nèi)部最大成員的整數(shù)倍.不足的要補(bǔ)?。

內(nèi)存對(duì)齊原因

提高cpu存取效率,保證數(shù)據(jù)讀取安全

常見對(duì)齊

  • 堆上的對(duì)象內(nèi)存以16字節(jié)對(duì)齊
  • 對(duì)象的成員變量內(nèi)存以8字節(jié)對(duì)齊
  • 對(duì)象與對(duì)象之間內(nèi)存以16字節(jié)對(duì)齊
?著作權(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),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

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