iOS内存对齐

535 阅读7分钟

一、什么是内存对齐

内存对齐(Memory alignment),也叫字节对齐。

现代计算机中内存空间都是按照 byte 划分的,从理论上讲似乎对任何类型的变量的访问可以从地址开始,但实际情况是在访问特定类型变量的时候经常在特定的内存地址访问,这就需要各种类型数据按照一定的规则在空间上排列,而不是顺序的一个接一个的排放,这就是对齐。

举一个简单的例子, 64位系统,int所占内存空间为 4 byteschar1 byte。如果把它们放在一个结构体中,则所占的内存空间应该是 4 + 1 = 5 bytes 。而事实上,在Xcode环境,sizeof 操作的结果都是 8 bytes

image.png

image.png

二、为什么要进行内存对齐

之所以要内存对齐,有两方面的原因:

平台原因:各个硬件平台存储空间处理上有很大的不同。一些平台某些特定类型数据只能从某些特定地址开始存取。比如,有些架构的CPU在访问一个没有进行对齐变量的时候会发生错误,那么在这种架构下编程必须保证字节对齐

性能原因:内存对齐可以提高存取效率。比如,有些平台每次读都是从偶地址开始,如果一个int型(假设为32位系统)如果存放在偶地址开始的地方,那么一个读周期就可以读出这32bit,而如果存放在奇地址开始的地方,就需要2个读周期,并对两次读出的结果的高低字节进行拼凑才能得到该32bit数据。

三、内存对齐的原则

1.数据成员对齐规则:结构(struct)(或联合(union))的数据成员,第一个数据成员放在offset0的地方,以后每个数据成员存储的起始位置要从该成员大小或者成员的子成员大小(只要该成员有子成员,比如说是数组,结构体等)的整数倍开始(比如int32 位机为字节,则要从的整数倍地址开始存储,再比如short32 位机为2字节,则要从2的整数倍地址开始存储)。

2.结构体作为成员:如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储.(struct a里存有struct b,b里有char,int ,double等元素,那b应该从8的整数倍开始存储.)

3.收尾工作:结构体的总大小,也就是sizeof的结果,必须是其内部最大成员的整数倍.不足的要补齐。

字节表

image.png

四、内存对齐底层探究

首先为什么要探索结构体内存对齐呢?因为看objc源码会发现万物皆对象基础是个结构体,当我们创建对象的时候,不需要去注意属性的先后顺序,因为系统会自动帮我们处理,但是当我们创建结构体的时候就需要我们去分析了,因为系统不会自动给我们优化,

先来看下下面两个结构体(无嵌套)

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


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

}LGStruct2;

我们声明以上两个结构体,两个结构体拥有的数据类型是一摸摸一样样的,理论上内存大小也应该一样(内存是不是真的一样大呢?),然后先看下以下代码

-----16-----24

那么就有问题了,为什么相同的数据类型,里面属性个数也是一样的,但是占用的空间大小不一样呢?这就是今天的重点,结构体内存对齐原则(上面第三点

下面就根据内存对齐原则进行简单的计算和分析
LGStruct2内存大小详细过程(min(m,n) m表示当前开始的位置,n表示大小) 变量a: 占8个字节,offert0开始, min(0,8), 即0 ~ 7 存放a
变量b: 占4个字节,offert8开始(12可以整除4), min(8,4), 即8 ~ 11 存放b
变量c: 占2个字节,offert12开始(12可以整除2),min(12,2),即12 ~ 13 存放c
变量d: 占1个字节,offert14开始(14可以整除1),min(14,1),即14 存放d\

结果显示 LGStruct1 的实际的内存大小是15字节,LGStruct1中最大的变量是a占个 8 字节。所以LGStruct1的实际内存大小必须是8的整数倍,15不是8的整数倍,向上取整,不足的自动补齐为16字节。最后LWStruct1的内存大小为16字节。

LGStruct1解析图如下

image.png

LGStruct2内存大小详细过程
变量a: 占8个字节,offert0开始, min(0,8), 即0 ~ 7 存放a
变量d: 占1个字节,offert8开始(8可以整除1), min(8,1), 即8 存放d
变量b: 占4个字节,offert9开始(9不可以整除4), min(9,4)9 % 4 != 0,继续往后移动直到找到可以整除4的位置 1212 ~ 15 存放b
变量c: 占2个字节,offert16开始(16可以整除2),min(16,2),即16 ~ 17 存放c

结果显示 LGStruct2 的实际的内存大小是18字节,LGStruct2中最大的变量是a占个 8 字节。所以LGStruct2的实际内存大小必须是8整数倍,18不是8整数倍,向上取整,不足的自动补齐为24字节。最后LGStruct2的内存大小为24字节。

LGStruct2解析图如下

image.png

结构体中嵌套结构体

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


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

struct LGStruct3{
    long    a; // 8
    int     b; // 4
    short   c; // 2
    char    d; // 1
    struct LGStruct2 lwStr;
}LGStruct3;

int main(int argc, char * argv[]) {
    @autoreleasepool {
      NSLog(@"-----%lu-----%lu----%lu",sizeof(LGStruct1),sizeof(LGStruct2),sizeof(LGStruct3));
    }
    return 0;
}

打印的结果

 -----16-----24----40

LGStruct3内存大小详细过程

变量a: 占8个字节,offert0开始, min(0,8), 即0 ~ 7 存放a
变量b: 占4个字节,offert8开始(8可以整除4), min(8,4), 即8 ~ 11 存放b
变量c: 占2个字节,offert12开始(12可以整除2),min(12,2),即12 ~ 13 存放c
变量d: 占1个字节,offert14开始(14可以整除1),min(14,1),即14 存放d\

变量lwStr:lwStr是结构体变量,内存对齐原则结构体成员要从其内部最大元素大小的整数倍地址开始存储。LGStruct2 中的最大的变量占8字节,所以offert16开始,LGStruct2的内存大小是18字节。min(16,18),即18 ~ 33存放 lwStr

结果显示 LGStruct3 的实际的内存大小是34字节,LGStruct3中最大的变量是lwStra都是 8 字节。所以LGStruct3的实际内存大小必须8整数倍34不是8的整数倍,向上取整,不足的自动补齐为40字节。最后LGStruct3的内存大小为40字节。

LGSTruct3解析图如下

image.png

五、总结

1、简单的结构体:第⼀个数据成员放在offset为0的地⽅,以后每个数据成员存储的起始位置要从该成员⼤⼩的整数倍 开始存储。
2、有嵌套结构体的结构体:则嵌套的结构体成员要从其内部最⼤元素⼤⼩的整数倍地址开始存储。
3、结构体的总⼤⼩,必须是其内部最⼤成员大小的整数倍,不⾜的要补全。

内存对齐有制定了一套规则,目的是提高cpu存取效率安全的访问。字节对齐可能浪费了部分内存,但是同时进行内存优化尽可能的降低了内存的浪费,即保证了存取的速率,又减少了内存的浪费,不得不说真的很优秀啊。