美文网首页
OC对象原理探索(中)-内存对齐

OC对象原理探索(中)-内存对齐

作者: U东东枪 | 来源:发表于2021-06-21 13:58 被阅读0次
    christopher-gower-m_HRfLhgABo-unsplash.jpg

    什么是内存对齐

    关于什么是内存对齐,我们通过下面一个例子来看一下

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

        //LMPerson 有两个成员变量: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 有三个成员变量 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
    

    为了更好的看懂打印的结果,我们先了解下sizeof,class_getInstanceSize,malloc_size

    • sizeof是C/C++中的关键字,它是一个运算符,其作用是取得一个对象(数据类型或者数据对象)的长度(即占用内存的大小,以byte为单位)。
    • class_getInstanceSize计算对象及成员变量占用的内存空间
    • malloc_size 系统实际分配的内存大小
      基础数据类型占用空间大小:
      基础数据类型占用空间大小

    我们来分析下打印的结果:

    1. 类的本质是一个结构体,这里的pstudent就是一个结构体指针,结构体指针的大小是8字节,所以sizeof打印都为8
    2. LMPerson 内有两个成员变量 name gendar 都是NSString类型,NSString类型占用8字节。
      LMStudent内有三个成员变量name gendar isAdult,name gendar 为``NSString类型各占8字节,isAdultBOOL类型占1字节。
      每个类里,苹果都设计包含一个isa,查看源码
    typedef struct objc_class *Class;
    
    /// Represents an instance of a class.
    struct objc_object {
        Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
    };
    

    发现isa是一个结构体指针,占用8个字节
    对比我们发现LMStudentLMPerson多一个BOOL类型的成员变量,内存居然相差了8个字节,说明内部进行了内存对齐,具体是不是这样,我们后面分析class_getInstanceSize探究结果
    3.p实际的内存地址是24,为什么系统分配的内存是32呢? 这里malloc_size也进行了内存对齐对齐方式可以看出是16位对齐,具体是不是这样,我们后面分析malloc_size探究结果

    在C/C++中,结构是一种复合数据类型,其构成元素既可以是基本数据类型(如intlongfloat等)的变量,也可以是一些复合数据类型(如数组、结构、联合等)的数据单元。元素是按照定义顺序一个一个放到内存中去的,但并不是紧密排列的。这就是 内存对齐

    为什么要内存对齐

    1.平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
    2.性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。

    内存对齐的规则

    1. 数据成员对⻬规则:结构(struct)(或联合(union))的数据成员,第 一个数据成员放在offset0的地方,以后每个数据成员存储的起始位置要 从该成员大小或者成员的子成员大小(只要该成员有子成员,比如说是数组,结构体等)的整数倍开始(比如int4字节,则要从4的整数倍地址开始存储)。
    2. 结构体作为成员:如果一个结构里有某些结构体成员,则结构体成员要从 其内部最大元素大小的整数倍地址开始存储.(struct a里存有struct b,b 里有char,int ,double等元素,那b应该从8的整数倍开始存储.)
    3. 结构体的总大小,也就是sizeof的结果,.必须是其内部最大成员的整数倍.不足的要补⻬。

    下面我们通过一个例子来分析结构体占用的实际内存大小

    struct LGStruct1 {
        double a;       // 8     double 类型在64位上占8个字节,位置为[0 7]
        char b;         // 1     char 类型在64位上占1个字节 第8位是1的倍数,所以在[8]
        int c;          // 4     int 类型在64位上占4个字节,下面第9,10,11位都不是4的倍数,所以跳过从12位开始存放(9 10 11 [12 13 14 15]
        short d;        // 2     short类型在64位上占2个字节,第16位是2的倍数,所以从16位开始存放位置为 [16 17],结构体内最大的占8个字节,结构体总大小要为8的倍数,所以结构体的总大小为 24
    }struct1;
    
    struct LGStruct2 {
        double a;       // 8    double 类型在64位上占8个字节,位置为[0 7]
        int b;          // 4    int 类型在64位上占4个字节,第8位是4的倍数,从第8位开始存放,位置为[8 9 10 11]
        char c;         // 1    char 类型在64位上占1个字节 第8位是1的倍数,所以位置为[12]
        short d;        // 2     short类型在64位上占2个字节,第13位不是2的倍数,所以从第14位开始存放(13 [14 15],结构体内最大的占8个字节,结构体总大小要为8的倍数,所以结构体的总大小为 16
    }struct2;
    struct LGStruct3 {
        double a;      // 8    double 类型在64位上占8个字节,位置为[0 7]
        int b;            // 4    int 类型在64位上占4个字节,第8位是4的倍数,从第8位开始存放,位置为[8 9 10 11]
        char c;         // 1    char 类型在64位上占1个字节 第8位是1的倍数,所以位置为[12]
        short d;       // 2     short类型在64位上占2个字节,第13位不是2的倍数,所以从第14位开始存放(13 [14 15]
        int e;           //4     int 类型在64位上占4个字节,第16位是4的倍数,所以从第16位开始存放[16 17 18 19]
        struct LGStruct1 str;    //24   上面已经分析 LGStruct1 实际占用24,  第20位不是24的倍数,所以 从 24位开始 (20 21 22 23 [24...47]  结构体str内最大的占8个字节,结构体总大小要为8的倍数,所以结构体的总大小为 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;
    

    打印验证一下:

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

    趁热打铁再分析几个:

    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源码确认是做了8字节对齐

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

    走到alignedInstanceSize方法内

        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字节对齐

    malloc_size 源码分析

    首先我们点击 malloc_size 发现源码在libmalloc,编译:

    malloc_size
    #import <Foundation/Foundation.h>
    #import <malloc/malloc.h>
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            //申请开辟40字节内存,系统最终开辟48字节
            void *p = calloc(1, 40);
            NSLog(@"系统分配的内存空间---%lu",malloc_size(p));
            //打印//系统分配的内存空间---48
        }
        return 0;
    }
    

    malloc_size是获取系统分配的内存大小 calloc是系统开辟内存,所以malloc_size底层流程其实是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;所以断定下面这行是关键代码
        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;
    }
    

    到这里我们发现zone->calloc点不进去了,怎么办呢,我们可以采用之前查看汇编的方法:打断点然后Debug->Debug Workflow -> Always show Disassembly:

    断点
    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);
    }
    

    这里同样可以通过汇编或者打印看下一步执行的步骤:

    default_zone_calloc
    nano_calloc
    _nano_malloc_check_clear
    因为我们只关心内存的变化,这里注意size_t 有一个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;
    }
    

    经过对齐算法,按照16字节对齐之后40字节按48字节开辟空间

    总结

    内存对齐规则

    • 数据成员对齐规则:从该成员大小或者成员的子成员大小整数倍开始。
    • 结构体作为成员对齐规则:结构体成员要从 其内部最大元素大小的整数倍地址开始存储。
    • 结构体的总大小,也就是sizeof的结果,.必须是其内部最大成员的整数倍.不足的要补⻬。

    内存对齐原因

    提高cpu存取效率,保证数据读取安全

    常见对齐

    • 堆上的对象内存以16字节对齐
    • 对象的成员变量内存以8字节对齐
    • 对象与对象之间内存以16字节对齐

    相关文章

      网友评论

          本文标题:OC对象原理探索(中)-内存对齐

          本文链接:https://www.haomeiwen.com/subject/qyoxyltx.html