[OC底层]内存字节对齐

简介

今天我们来学习下OC内存对齐。

先让你们脑瓜子嗡嗡嗡起来

  1. 为什么要内存对齐?

答: 为了提高效率,简单来说以空间换时间,这个主要跟芯片的设计相关。CPU执行程序的过程可以简单的分为四步

  1. CPU读取PC指针指向的命令,将它导入指令寄存器

  2. CPU确定指令寄存器中指令的类型、参数

  3. 分别给到不同单元执行。计算指令->逻辑运算单元;存储指令->控制单元

  4. PC指针自增,进行下一条指令。

而在汇编中,不同长度的内存访问会用到不同的汇编指令。如果一块内存是在地址上随便放的,CPU则需要多条不同的指令来访问,如果了解指令周期的同学应该知道,这会大大降低效率。

先上张图,复习下还给老师的知识(大神Cooci

image.png

结论先行

以下是内存对齐的原则

  1. 结构体的内存大小,并非其内部元素大小之和;1+8+4 != 16
  2. 结构体变量的起始地址,可以被最大元素基本类型大小或者模数整除;
  3. 结构体的内存对齐,按照其内部最大元素基本类型或者模数大小对齐;
  4. 模数在不同平台值不一样,也可通过预编译命令#pragma pack(n)方式去改变,iOS系统n=8;一个嵌入式gg的总结,拉到最后
  5. 如果空间地址允许,结构体内部元素会拼凑一起放在同一个对齐空间;
  6. 结构体内有结构体变量元素,其结构体并非展开后再对齐, 而是以结构体变量的最大内部元素大小或者模数对齐

验证结构体内存对齐

首先我们新建一个工程,声明三个struct,分别是FSStruct1FSStruct2FSStruct3。代码如下

struct LGStruct1 {
    double a;       // 8    [0 7]
    char b;         // 1    [8]
    int c;          // 4    (9 10 11 [12 13 14 15]
    short d;        // 2    [16 17] 24
}struct1;

struct LGStruct2 {
    double a;       // 8    [0 7]
    int b;          // 4    [8 9 10 11]
    char c;         // 1    [12]
    short d;        // 2    (13 [14 15] 16
    short e;        // 2    [17 18]
}struct2;

// 家庭作业 : 结构体内存对齐

struct LGStruct3 {
    double a; // 8    [0 7]
    int b;    // 4    [8 9 10 11]
    char c;    // 1    [12]
    short d;   // 2    (13 [14 15] 16
    int e;     // 4   [16 17 18 19] 20
    struct LGStruct1 str; // 8 (20 [24 25... 31] 
}struct3;
复制代码

然后在main.m中我们初始化这些结构体,根据打印以及调试分析实际内存地址分配以验证我们的观点。我们首先看Struct a与b,除了内部元素的顺序不一样之外,其它都是一致的,很奇怪哦,他两竟然内存大小不一致 a=24|b=16。截图打印输出内存地址 a=0x7ffee7a5cc20 b=0x7ffee7a5cc10 c=0x7ffee7a5cbe0那我们接下来先用结论分析一下 LGStruct1 a ; LGStruct2 b

image.png

根据内存对齐原则分析LGStruct1 a

  1. double a,占8个字节,从0开始,此时满足 min(0,8)模数整除,即[0,7]存放a;
  2. char b,占1个字节,从8开始,此时满足min(8,1)模数整除,即[8]存放b;
  3. int c,占4个字节,从9开始,此时不满足min(9,4)模数整除,则需要补齐到12才满足模数整除,即[12,15]存放c;
  4. short d,占2个字节,从16开始,此时满足min(16,2)模数整除,即[16,17]存放d;
  5. double a = 1.1 转化为十六进制 1.1000000000000001 -> 0x3ff199999999999a
  6. char b = ‘a’ 转化为十六进制 ASCII 96 -> 0x61
  7. int c = 2 转化为十六进制 2 -> 0x00000002
  8. short d = 3 转化为十六进制 3 -> 0x0003
  9. 根据小端模式修正以及8字节对齐排列,最后应该是 0x3ff199999999999a 0x0000000200000061 0x0000000000000003, 8+8+8=24字节

我们实际验证一下,lldb输入,x/4gx 0x7ffee7a5cc20,与我们的推测一一对应。

image.png

此时点根烟不抽,我们讲一下e -f f -- 0x3ff199999999999a,如果这里直接po是无法输出的,e是expression的简写,在lldb输入help expression可以得到详细的说明书,我们只抽用到的这个解释一下,第一个参数**-f是–format的简写代表输出格式 ,第二个参数f**是格式,这里代表的float,– 是格式要求见以下说明。

-f <format> ( --format <format> ) 
    Specify a format to be used for display. 
'f' or "float" 
Examples: 

     expr my_struct->a = my_array[3] 
     expr -f bin -- (index * 8) + 5 
     expr unsigned int $foo = 5 
     expr char c[] = \"foo\"; c[0] 
     
     Important Note: Because this command takes 'raw' input, if you use 
     any command options you must use ' -- ' between the end of the command options and the beginning of the raw input.
复制代码

那都说一个有可能是巧合,我们接着根据内存对齐原则分析 LGStruct2 b

  1. double a,占8个字节,从0开始,此时满足 min(0,8)模数整除,即[0,7]存放a;
  2. int b,占4个字节,从8开始,此时满足min(8,4)模数整除,即[8,11]存放b;
  3. char c,占1个字节,从12开始,此时满足min(12,1)模数整除,即[12]存放c;
  4. short d,占2个字节,从13开始,此时不满足min(13,2)模数整除,则需要补齐到14才满足模数整除,即[14,15]存放d;
  5. double a = 1.1 转化为十六进制 1.1000000000000001 -> 0x3ff199999999999a;
  6. int b = 2 转化为十六进制 2 -> 0x00000002;
  7. char c = ‘b’ 转化为十六进制 ASCII 97 -> 0x62
  8. short d = 3 转化为十六进制 3 -> 0x0003
  9. 根据小端模式修正以及8字节对齐排列,最后应该是 0x3ff199999999999a 0x0003006200000002,8+8=16字节

同样的,我们实际验证一下,lldb输入,x/4gx 0x7ffee27acc10,也是与我们的推测一一对应

image.png

此时1-5的结论基本被论证,我们接下来分析一下结构体套结构体的 LGStruct3 c

  1. double a,占8个字节,从0开始,此时满足 min(0,8)模数整除,即[0,7]存放a;
  2. int b,占4个字节,从8开始,此时满足min(8,4)模数整除,即[8,11]存放b;
  3. char c,占1个字节,从12开始,此时满足min(12,1)模数整除,即[12]存放c;
  4. short d,占2个字节,从13开始,此时不满足min(13,2)模数整除,则需要补齐到14才满足模数整除,即[14,15]存放d;
  5. int e,占4个字节,从16开始,此时满足min(16,4)模数整除,即[16,19]存放e;
  6. struct FSStruct1 str,占15个字节,FSStruct1元素最大为8,从20开始,此时不满足min(20,8)模数整除,则需要补齐到24才满足模数整除,即[24, 47]; FSStruct1见上面分析,大小24字节,此处一样的计算规则;
  7. double a = 1.1 转化为十六进制 1.1000000000000001 -> 0x3ff199999999999a;
  8. int b = 2 转化为十六进制 2 -> 0x0002;
  9. char c = ‘b’ 转化为十六进制 ASCII 97 -> 0x62;
  10. short d = 3 转化为十六进制 3 -> 0x0003;
  11. int e = 4 转化为十六进制 4-> 0x00000004;
  12. struct FSStruct1 str,重复 FSStruct1 a的步骤1-9,应该是0x3ff199999999999a 0x0000000200000061 0x0000000000000003
  13. 根据小端模式修正以及8字节对齐,最后应该是 0x3ff199999999999a 0x0003006300000002 0x0000000000000004 0x3ff199999999999a 0x0000000200000061 0x0000000000000003, 8 * 6 = 48字节

image.png

验证iOS对象内存对齐

弄完结构体,其实我们日常iOSer用struct应该不多,还是OC对象用起来比较多,我们还是需要分析一下OC对象。所以为了排除掉一些不重要的因素,我们新建一个FSStudent里面属性保持与Struct一致。代码如下

@interface FSStudent : NSObject 
@property (nonatomic) double a; 
@property (nonatomic) char b; 
@property (nonatomic) int c; 
@property (nonatomic) short d; 
@end 

@interface FSStudent2 : NSObject 
@property (nonatomic) double a; 
@property (nonatomic) int b; 
@property (nonatomic) char c; 
@property (nonatomic) short d; 
@end 

@interface FSStudent3 : NSObject 
@property (nonatomic) double a; 
@property (nonatomic) int b; 
@property (nonatomic) char c; 
@property (nonatomic) short d; 
@property (nonatomic) int e; 
@property (nonatomic, strong) FSStudent *str; 
@end
复制代码

我们也借助打印看下结果,sizeof打印的都是8,这个代表是指针类型占用的大小;class_getInstanceSize打印的是24、24、40,这个是获取到的实例对象所占用的内存大小,8字节对齐;malloc_size打印的是32、32、48,这个是系统实际分配的内存大小,16字节对齐

image.png

对象我们就不分析了,直接用工具推倒,先来看看 FSStudent a ,如下所示 0x00000001054faca0 是isa指针,0x0000000200030061 很清晰的可以想到是 0x00000002 、0x0003 与 0x61(对应b=’a‘,c=2,d=3)的组合,苹果针对属性进行了内存重排。因为b、c、d分别对应char(1)、int(4)、short(2)个字节, 1+2+4按照 8字节对齐 ,不足补齐的存放在同一块内存中。实际对象所占内存为 8*3=24 字节,遵循16字节对齐,实际分配了8*4=32字节,后面用0补齐;而对于不通属性排列的OC对象
FSStudent2 b 我们可以看到实际对象所占内存大小与实际分配大小都与 a 一致,而且内存中排列除了0x61、0x62实际值不一样,其它排列都保持一致。在这里我们可以确认的是苹果在OC对象的属性做了重排,优化了内存分配。

(lldb) po a <FSStudent: 0x600000237ac0> 

(lldb) x/8gx 0x600000237ac0 
0x600000237ac0: 0x00000001054faca0 0x0000000200030061 
0x600000237ad0: 0x3ff199999999999a 0x0000000000000000 

(lldb) po b 
<FSStudent2: 0x600000237b20> 

(lldb) x/8gx 0x600000237b20 
0x600000237b20: 0x00000001054facf0 0x0000000200030062 
0x600000237b30: 0x3ff199999999999a 0x0000000000000000
复制代码

总结

  1. struct 针对相同数据类型的元素按照不同的顺序排列,会分配不一样的内存大小,所以良好的意识与代码习惯会优化程序性能。笔者是从事智能硬件、音视频相关行业的,一些摄像头的嵌入式开发,内存资源匮乏显得这项尤为重要。
  2. 苹果iOS系统对于对象的属性进行了重排,作用是优化内存。其实这种系统的优化还有很多,举个栗子,NSArray也是基于C的数组进行了优化,有兴趣的同学可以摸索一下。平时开发可能用不上,但是这种深入的探索百利无一害。
  3. 针对2说的探索讲一下一个以前学到的,探索汇编之后发现if 与 Switch case这种简单语法针对不同的场景也可以做优化,可能就是调整一下顺序,也可以优化性能节省算力。
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享