OC底层原理02:内存对齐

905 阅读6分钟

1.为什么要内存对齐?

struct Struct1{
    double a; // 8字节 
    char b;   // 1字节 
    int c;    // 4字节 
    short d;  // 2字节 
}struct1;

内存存取粒度:CPU读取内存时,以'一块一块'的进行读取,‘块’的大小可以是1、2、4、8、16...个字节,这样的块就是内存读取粒度

  • 假如在没有内存对齐的情况下,读取如上的数据时,会是什么样的呢?

CPU在读取数据时,首先从内存读取粒度8开始读取,读出了a的数据,然后使用同样的读取粒度读取后边的数据时,发现b、c、d读不到,所以要修改内存读取粒度,从8降到4,这时读取b就包含了c的数据,这是不对的,直到修改内存读取粒度为1时,才能读得到b,接下来读c时,内存读取粒度就不够了,需要加大粒度值两次到4,才能读取到c...

由上我们可以知道,每次在读取时,我们可能都要同步调整内存读取粒度,才不会读取越界,这样CPU的内存访问速度会受到非常大的影响,读取的时间会很长,并且出错的风险也会很大.


2.内存对齐的规则

  1. 结构(struct)或联合(union))的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员存储的起始位置要从该成员大小或者成员的子成员的整数倍开始
  2. 如果一个结构体里存在结构体成员,则结构体成员要从其内部最大成员数据类型所占内存大小的整数倍地址开始存储
  3. 结构体的总大小,也就是sizeof的结果,必须是其内部最大成员的整数倍,不足要补齐

由内存对齐的规则,我们再来看上边的结构体是如何对齐存储的.

    1. double a 由规则1,第一个数据成员a,会从偏移量0的位置开始,占据了8个字节
    1. char b 当存储到char b时,CPU读到了8的位置,8作为1的整数倍(char 为1字节),可以直接从8的位置存储一个字节
    1. int c 当存储到int c时,CPU读到了9的位置,由规则1数据成员存储的起始位置要从该成员大小的整数倍开始,9并不是int大小(4)的整数倍,则padding,10、11同理,直到12的位置,才会开始存储int c
    1. short d 当存储到short d时,CPU读到了16的位置,16是short大小(2)的整数倍,可以直接存储

那么,此时这个struct的总大小,由规则3总大小必须是其内部最大成员的整数倍,不足要补齐,其中最大的数据成员是double 8个字节,最后的成员数据d的位置在17.以8字节最大整数倍补齐,所以这个结构体的总大小为24字节.

此时内存对齐后读取数据时,使用8个内存读取粒度读取a; 读取b、c时只会修改一次内存读取粒度4,读取b时会把9、10、11也读下来,虽然浪费了三个字节的内存,但是同样也提高了读取的效率.利用空间换时间来进行优化.


3. 属性重排优化

struct Struct2{
    double a; // 8字节 
    int c;    // 4字节 
    short d;  // 2字节 
    char b;   // 1字节 
}struct2;

Struct2和Struct1中数据成员类型一模一样,唯一不同的是排列的顺序差异.依据同样的对齐规则存储结果如下: 所以Struct2的总大小为16字节.

这里可以得到这样的结论:结构体内存大小与结构体成员内存大小的顺序有关

  • 案例支持 这里我们看到0x0000001300006261指针指向的是乱码数据,那么,person的int属性age,char属性c1、c2在哪?

OC对象的本质就是一个结构体

如上图我们可以看到,person的int属性age,char属性c1、c2重排优化在了一个16位数据中.c1,c2的结果是以ASCII码表示.


结构体拓展1:

struct Struct3{
    double a; 				// 8字节 
    char b;   				// 1字节 
    int c;    				// 4字节 
    short d;  				// 2字节 
    struct Struct1 str;     // 
}struct3;

Struct3结构体中也包含了Struct1结构体,这样要如何进行内存对齐呢?

a、b、c、d的内存对齐同上,这里我们主要看结构体数据成员str.

由内存对齐规则二:

如果一个结构体里存在结构体成员,则结构体成员要从其内部最大成员数据类型所占内存大小的整数倍地址开始存储. Struct1结构体中最大的数据类型是double--8字节,存储到d的位置在17.所以存储Struct1结构体第一个数据double a时,18、19、20、21、22、23都会被padding.具体如下:

同样由内存对齐规则三:结构体的总大小,必须是其内部最大成员的整数倍,不足要补齐.所以Struct3结构体的总大小为48.


结构体拓展2:

struct Struct1{
    char b;
    int c;
    short d;
}struct1;

struct Struct2{
    int c;
    double a;
    char b;
    struct Struct1 struct1;
    short d;
}struct2;

以上两个结构体,同样是结构体struct2中包含了结构体struct1作为数据成员,不过并不是放在最后一个位置。这样存储结构体struct2,结果会有什么不一样吗?

struct2中,c、a、b存储后的结果到16个字节。重点分析之后如何存储:

  1. 由内存对齐规则二:如果一个结构体里存在结构体成员,则结构体成员要从其内部最大成员数据类型所占内存大小的整数倍地址开始存储.所以struct1开始存储的位置不从17而是从20开始。如下:

注意

此时存储到最后一个数据成员short d时,并不是从30开始存储,因为结构体struct1内部还未做到的内存对齐

  1. 由内存对齐规则三: struct的总大小必须是内部最大成员的整数倍,不足要补齐。所以当struct1以4字节最大整数倍补齐后,所以struct1的总大小为12字节。

然后从32开始存储struct2最后一个数据成员short d,存储位置为[32,33]。同样由内存对齐规则三,struct2以8字节最大整数倍补齐后,struct2的总大小为40字节。



数据类型表参考: