简介
今天我们来学习下OC内存对齐。
先让你们脑瓜子嗡嗡嗡起来
- 为什么要内存对齐?
答: 为了提高效率,简单来说以空间换时间,这个主要跟芯片的设计相关。CPU执行程序的过程可以简单的分为四步
-
CPU读取PC指针指向的命令,将它导入指令寄存器
-
CPU确定指令寄存器中指令的类型、参数
-
分别给到不同单元执行。计算指令->逻辑运算单元;存储指令->控制单元
-
PC指针自增,进行下一条指令。
而在汇编中,不同长度的内存访问会用到不同的汇编指令。如果一块内存是在地址上随便放的,CPU则需要多条不同的指令来访问,如果了解指令周期的同学应该知道,这会大大降低效率。
先上张图,复习下还给老师的知识(大神Cooci)
结论先行
以下是内存对齐的原则
- 结构体的内存大小,并非其内部元素大小之和;1+8+4 != 16
- 结构体变量的起始地址,可以被最大元素基本类型大小或者模数整除;
- 结构体的内存对齐,按照其内部最大元素基本类型或者模数大小对齐;
- 模数在不同平台值不一样,也可通过预编译命令#pragma pack(n)方式去改变,iOS系统n=8;一个嵌入式gg的总结,拉到最后
- 如果空间地址允许,结构体内部元素会拼凑一起放在同一个对齐空间;
- 结构体内有结构体变量元素,其结构体并非展开后再对齐, 而是以结构体变量的最大内部元素大小或者模数对齐
验证结构体内存对齐
首先我们新建一个工程,声明三个struct,分别是FSStruct1
、FSStruct2
、FSStruct3
。代码如下
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
根据内存对齐原则分析LGStruct1 a
- double a,占8个字节,从0开始,此时满足 min(0,8)模数整除,即[0,7]存放a;
- char b,占1个字节,从8开始,此时满足min(8,1)模数整除,即[8]存放b;
- int c,占4个字节,从9开始,此时不满足min(9,4)模数整除,则需要补齐到12才满足模数整除,即[12,15]存放c;
- short d,占2个字节,从16开始,此时满足min(16,2)模数整除,即[16,17]存放d;
- double a = 1.1 转化为十六进制 1.1000000000000001 -> 0x3ff199999999999a
- char b = ‘a’ 转化为十六进制 ASCII 96 -> 0x61
- int c = 2 转化为十六进制 2 -> 0x00000002
- short d = 3 转化为十六进制 3 -> 0x0003
- 根据小端模式修正以及8字节对齐排列,最后应该是
0x3ff199999999999a 0x0000000200000061 0x0000000000000003
, 8+8+8=24字节
我们实际验证一下,lldb输入,x/4gx 0x7ffee7a5cc20
,与我们的推测一一对应。
此时点根烟不抽,我们讲一下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
- double a,占8个字节,从0开始,此时满足 min(0,8)模数整除,即[0,7]存放a;
- int b,占4个字节,从8开始,此时满足min(8,4)模数整除,即[8,11]存放b;
- char c,占1个字节,从12开始,此时满足min(12,1)模数整除,即[12]存放c;
- short d,占2个字节,从13开始,此时不满足min(13,2)模数整除,则需要补齐到14才满足模数整除,即[14,15]存放d;
- double a = 1.1 转化为十六进制 1.1000000000000001 -> 0x3ff199999999999a;
- int b = 2 转化为十六进制 2 -> 0x00000002;
- char c = ‘b’ 转化为十六进制 ASCII 97 -> 0x62
- short d = 3 转化为十六进制 3 -> 0x0003
- 根据小端模式修正以及8字节对齐排列,最后应该是
0x3ff199999999999a 0x0003006200000002
,8+8=16字节
同样的,我们实际验证一下,lldb输入,x/4gx 0x7ffee27acc10
,也是与我们的推测一一对应
此时1-5的结论基本被论证,我们接下来分析一下结构体套结构体的 LGStruct3 c
- double a,占8个字节,从0开始,此时满足 min(0,8)模数整除,即[0,7]存放a;
- int b,占4个字节,从8开始,此时满足min(8,4)模数整除,即[8,11]存放b;
- char c,占1个字节,从12开始,此时满足min(12,1)模数整除,即[12]存放c;
- short d,占2个字节,从13开始,此时不满足min(13,2)模数整除,则需要补齐到14才满足模数整除,即[14,15]存放d;
- int e,占4个字节,从16开始,此时满足min(16,4)模数整除,即[16,19]存放e;
- struct FSStruct1 str,占15个字节,
FSStruct1
元素最大为8,从20开始,此时不满足min(20,8)模数整除,则需要补齐到24才满足模数整除,即[24, 47];FSStruct1
见上面分析,大小24字节,此处一样的计算规则; - double a = 1.1 转化为十六进制 1.1000000000000001 -> 0x3ff199999999999a;
- int b = 2 转化为十六进制 2 -> 0x0002;
- char c = ‘b’ 转化为十六进制 ASCII 97 -> 0x62;
- short d = 3 转化为十六进制 3 -> 0x0003;
- int e = 4 转化为十六进制 4-> 0x00000004;
- struct FSStruct1 str,重复
FSStruct1 a
的步骤1-9,应该是0x3ff199999999999a 0x0000000200000061 0x0000000000000003
- 根据小端模式修正以及8字节对齐,最后应该是
0x3ff199999999999a 0x0003006300000002 0x0000000000000004 0x3ff199999999999a 0x0000000200000061 0x0000000000000003
, 8 * 6 = 48字节
验证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字节对齐
对象我们就不分析了,直接用工具推倒,先来看看 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
复制代码
总结
- struct 针对相同数据类型的元素按照不同的顺序排列,会分配不一样的内存大小,所以良好的意识与代码习惯会优化程序性能。笔者是从事智能硬件、音视频相关行业的,一些摄像头的嵌入式开发,内存资源匮乏显得这项尤为重要。
- 苹果iOS系统对于对象的属性进行了重排,作用是优化内存。其实这种系统的优化还有很多,举个栗子,NSArray也是基于C的数组进行了优化,有兴趣的同学可以摸索一下。平时开发可能用不上,但是这种深入的探索百利无一害。
- 针对2说的探索讲一下一个以前学到的,探索汇编之后发现if 与 Switch case这种简单语法针对不同的场景也可以做优化,可能就是调整一下顺序,也可以优化性能节省算力。