iOS底层原理--内存对齐

在iOS底层原理--alloc&init&new这篇文章中,我们认识到了字节对齐
那么,我们回顾一下什么是字节对齐。

字节对齐

假如一个创建一个对象LGPerson

    //创建LGPerson
     LGPerson *p1 = [LGPerson alloc];

通过调用

//获取size
size = cls->instanceSize(extraBytes);

之后得到size = 16

接下来我们调用class_getInstanceSize(这个方法是获取类对象的实际内存)

// size  = 8
size_t size = class_getInstanceSize([LGPerson class])

最终得到的size = 8

这就验证了字节对齐的存在,而且由于实际大小为8,所以字节对齐的最小值是16个字节。

内存对齐

很多 CPU拒绝读取未对齐数据。当一个程序要求这些 CPU 读取未对齐数据时,这时 CPU 会进入异常处理状态并且通知程序不能继续执行。而且读取未对齐的数据,会大大降低 CPU 的性能。
CPU把内存当成是一块一块的,块的大小可以是2,4,8,16字节大小,因此CPU在读取内存时是一块一块进行读取的。每次内存存取都会产生一个固定的开销,减少内存存取次数将提升程序的性能。所以 CPU 一般会以 2/4/8/16/32 字节为单位来进行存取操作。我们将上述这些存取单位也就是块大小称为(memory access granularity)内存存取粒度。

我们用一段代码来解释内存对齐

  • 首先我们定义2个结构体
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)打印结构体的内存大小,照理说2个结构体的内容一样,只是排布顺序不同,大小应该一致。那么打印结果如下:

打印结构体.png

明显看到,2个结构体的内存大小不一样,这就是内存对齐产生的影响。

内存对齐的原则

  • 数据成员对⻬规则:结构(struct)(或联合(union))的数据成员,第⼀个数据成员放在offset为0的地⽅,以后每个数据成员存储的起始位置要从该成员⼤⼩或者成员的⼦成员⼤⼩(只要该成员有⼦成员,⽐如说是数组,结构体等)的整数倍开始(⽐如int为4字节,则要从4的整数倍地址开始存储。
  • 结构体作为成员:如果⼀个结构⾥有某些结构体成员,则结构体成员要从其内部最⼤元素⼤⼩的整数倍地址开始存储.(struct a⾥存有struct b,b⾥有char,int ,double等元素,那b应该从8的整数倍开始存储.)
  • 收尾⼯作:结构体的总⼤⼩,也就是sizeof的结果,.必须是其内部最⼤成员的整数倍.不⾜的要补⻬。

内存对齐的解释

上面的话太官方了,我们用比较通俗的语言来解释。

针对上面struct1这个案例来分析,首先我们已经标注出每个成员所占的内存大小。

struct LGStruct1 {
    double a;   //占8位
    char b;     //占1位
    int c;      //占4位
    short d;    //占2位
}struct1;

具体类型在c/oc中所占内存大小如图:


各数据类型所占内存.png

所以按照规则来进行操作:

  • 第⼀个数据成员放在offset为0的地⽅,我们从0开始,double 占8个字节
    double a.png

如上图,a从0开始,长度为8,所以截止到7

  • 每个数据成员存储的起始位置要从该成员⼤⼩或者成员的⼦成员⼤⼩,比如第二个成员为char,char的长度为1,所以从8开始即可

    char b.png

    如上图,b从8开始,长度为1,所以截止到9

  • 还是上面的规则,第三个成员为int,int的长度为4,所以要从4的倍数的位置开始,而邻近的4的倍数的位置为12,所以从12起始

    int c.png

    如上图,c从12开始,长度为4,所以截止到15

  • 第四个成员为short,short的长度为2,所以要从2的倍数的位置开始,而邻近的2的倍数的位置为16,所以从16起始

    short d.png

    如上图,d16开始,长度为2,所以截止到17

  • 最后,结构体的总⼤⼩,也就是sizeof的结果,.必须是其内部最⼤成员的整数倍。不⾜的要补⻬。对于上面的struct1,内部最大成员为a = 8,它的整数倍为8 * 3 = 24

    最终长度.png

所以最终长度为24。

接下来,我们用上面的规则来看下struct2这个例子。

struct LGStruct2 {
    double a;    //占8位
    int b;       //占4位
    char c;      //占1位
    short d;     //占2位
}struct2;

还是通过示意图来来解读。


struct2

最终得出了struct2的长度为16。

结构体的嵌套

接下来,我们来下面这个例子,结构体struct2中嵌套了一个结构体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,如下图:
    结构体.png
  • 接下来嵌入体,规则为结构体作为成员:如果⼀个结构⾥有某些结构体成员,则结构体成员要从其内部最⼤元素⼤⼩的整数倍地址开始存储。那么针对嵌套的struct1来说,里面最大的元素为double a,长度为8。所以最临近的点为16,所以结构体从16开始。
    结构体.png

    如上图所示,实际需要的内存大小为33个字节。
  • 最后,结构体的总⼤⼩,也就是sizeof的结果,.必须是其内部最⼤成员的整数倍。不⾜的要补⻬。按照最大长度的整数倍,所以计算得出8 * 5 = 40,最终长度为40个字节。
    最终长度.png

所以,再看内存对齐的规则会更加清晰。

  • 数据成员对⻬规则:结构(struct)(或联合(union))的数据成员,第⼀个数据成员放在offset为0的地⽅,以后每个数据成员存储的起始位置要从该成员⼤⼩或者成员的⼦成员⼤⼩(只要该成员有⼦成员,⽐如说是数组,结构体等)的整数倍开始(⽐如int为4字节,则要从4的整数倍地址开始存储。
  • 结构体作为成员:如果⼀个结构⾥有某些结构体成员,则结构体成员要从其内部最⼤元素⼤⼩的整数倍地址开始存储.(struct a⾥存有struct b,b⾥有char,int ,double等元素,那b应该从8的整数倍开始存储.)
  • 收尾⼯作:结构体的总⼤⼩,也就是sizeof的结果,.必须是其内部最⼤成员的整数倍.不⾜的要补⻬。

属性重排(内存优化)

我们知道,所有的oc对象本质上是一个结构体,那么如果按照内存对齐对齐的原则的话,我们一定要特别注意属性存放的位置。因为结构体的大小,和结构体成员的排列顺序有关。
但是实际上,并没有人去关注对象属性的位置。这就是属性重排的作用。
通过一个例子来看。

  • 我们定义一个对象
#import 

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';
  • 接下来打印数据,po得出LGPerson的内存地址

    po.png

  • 通过x/4gx找出LGPerson的属性地址

    x/4gx.png

  • 我们分别打印4个内存地址


    打印内存地址.png

    分别能得出LGPersonKCCooci。但是77309436513这一串是个乱码

  • 我们把0x0000001200006261拆分成0x00000012,0x62,0x61,分别得到

    image.png

    可以得到18,a,b了。

通过ASCII换算得出97为a,98为b。

我们用一张图来表示


LGPerson.png

对象的内存对齐

我们用一段代码来展示,还是之前的对象,我们来打印一下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)));

打印结果如下:


LGPerson.png

对打印信息进行分析

  • NSLog(@"%@", person)
    这个打印出来的结果就是LGPerson,包括它的地址信息。

  • NSLog(@"%lu",sizeof(person))
    person实际上是一个指向LGPerson的指针,一个指针有8个字节,所以大小为8。不管LGPerson有多大,person的大小一直为8。

  • NSLog(@"%lu",class_getInstanceSize([LGPerson class]))
    class_getInstanceSize这个方法的作用就是返回对象真正需要的内存,进行了长度为8的字节对齐。看源码,点击进去,实际走到了这个方法:

//定义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

转换成二进制

//31的二进制
0000 0000 0001 1111

~WORD_MASK表示WORD_MASK的二进制取反

//7的二进制取反
1111 1111 1111 1000

最后经过与运算,得到

   //31的二进制
   0000 0000 0001 1111
   //7的二进制取反
&1111 1111 1111 1000
   //最终结果
= 0000 0000 0001 1000

可以看到,前面3位都抹0,所以可以得出结论,对象创建的时候,真正进行的是8位的字节对齐

  • NSLog(@"%lu",malloc_size((__bridge const void *)(person)))
    通过malloc算法进行研究

目前已知的16字节内存对齐算法有两种:
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,得出的结果为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,所以可以得出结论,系统进行内存分配的时候,进行了16位的内存对齐。

结论

虽然我们的结构体遵循内存对齐的原则,但是,它不会随意的浪费内存,会通过内存重排的方式进行结构优化,将多余的内存进行合理的利用,尽最大的可能节省内存。

你可能感兴趣的:(iOS底层原理--内存对齐)