在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个结构体的内容一样,只是排布顺序不同,大小应该一致。那么打印结果如下:
明显看到,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中所占内存大小如图:
所以按照规则来进行操作:
- 第⼀个数据成员放在offset为0的地⽅,我们从0开始,
double 占8个字节
如上图,a从0开始,长度为8,所以截止到7。
-
每个数据成员存储的起始位置要从该成员⼤⼩或者成员的⼦成员⼤⼩,比如第二个成员为
char
,char的长度为1,所以从8开始即可
如上图,b从8开始,长度为1,所以截止到9 -
还是上面的规则,第三个成员为
int
,int的长度为4,所以要从4的倍数的位置开始,而邻近的4的倍数的位置为12,所以从12起始
如上图,c从12开始,长度为4,所以截止到15 -
第四个成员为
short
,short的长度为2,所以要从2的倍数的位置开始,而邻近的2的倍数的位置为16,所以从16起始
如上图,d16开始,长度为2,所以截止到17。 -
最后,结构体的总⼤⼩,也就是sizeof的结果,.必须是其内部最⼤成员的整数倍。不⾜的要补⻬。对于上面的
struct1
,内部最大成员为a = 8
,它的整数倍为8 * 3 = 24
。
所以最终长度为24。
接下来,我们用上面的规则来看下struct2
这个例子。
struct LGStruct2 {
double a; //占8位
int b; //占4位
char c; //占1位
short d; //占2位
}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
,如下图:
- 接下来嵌入体,规则为结构体作为成员:如果⼀个结构⾥有某些结构体成员,则结构体成员要从其内部最⼤元素⼤⼩的整数倍地址开始存储。那么针对嵌套的
struct1
来说,里面最大的元素为double a
,长度为8
。所以最临近的点为16,所以结构体从16开始。
如上图所示,实际需要的内存大小为33个字节。 - 最后,结构体的总⼤⼩,也就是sizeof的结果,.必须是其内部最⼤成员的整数倍。不⾜的要补⻬。按照最大长度的整数倍,所以计算得出
8 * 5 = 40
,最终长度为40个字节。
所以,再看内存对齐
的规则会更加清晰。
- 数据成员对⻬规则:结构(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
的内存地址
-
通过
x/4gx
找出LGPerson
的属性地址
-
我们分别打印4个内存地址
分别能得出
LGPerson
、KC
、Cooci
。但是77309436513
这一串是个乱码 -
我们把
0x0000001200006261
拆分成0x00000012
,0x62
,0x61
,分别得到
可以得到18
,a
,b
了。
通过ASCII换算得出97为a,98为b。
我们用一张图来表示
对象的内存对齐
我们用一段代码来展示,还是之前的对象,我们来打印一下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)));
打印结果如下:
对打印信息进行分析
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位的内存对齐。
结论
虽然我们的结构体遵循内存对齐的原则,但是,它不会随意的浪费内存,会通过内存重排的方式进行结构优化,将多余的内存进行合理的利用,尽最大的可能节省内存。