目录
1:内存对齐的原因
2:内存对齐的规则
3:结构体内存分配演练以及在iOS中对象成员的内存分配探索
一 :内存对齐的原因
计算机内存是以字节(Byte)为单位划分的,理论上CPU可以访问任意编号的字节,但实际情况并非想象中的一个一个字节取出拼接的,而是根据自己的字长来独处数据的。
我们都知道CPU的数据总线宽度决定了CPU对数据的吞吐量,例如:64位CPU一次可以处理64bit也就是8个字节的数据,32位一个道理,每次可以处理4个字节的数据。
以32位的CPU为例,实际寻址的步长为4个字节,也就是只对编号为 4 的倍数的内存寻址,例如 0、4、8、12、1000 等,而不会对编号为 1、3、11、1001 的内存寻址。如下图所示:
这样做可以实现最快速的方式寻址且不会遗漏一个字节,也不会重复寻址。
那么对于程序而言,一个变量的数据存储范围是在一个寻址步长范围内的话,这样一次寻址就可以读取到变量的值,如果是超出了步长范围内的数据存储,就需要读取两次寻址再进行数据的拼接,效率明显降低了。例如一个double类型的数据在内存中占据8个字节,如果地址是8,那么好办,一次寻址就可以了,如果是20呢,那就需要进行两次寻址了。这样就产生了数据对齐的规则,也就是将数据尽量的存储在一个步长内,避免跨步长的存储,这就是内存对齐。在32位编译环境下默认4字节对齐,在64位编译环境下默认8字节对齐。
二 :内存对齐的规则
每个特定平台上的编译器都有自己的默认“对齐系数”(也叫对齐模数)。程序员可以通过预编译命令#pragma pack(n)。在iOS平台中默认的对齐系数是8
1、数据成员对齐规则:(Struct 或 union 的数据成员)第一个数据成员放在偏移为0的位置,以后每个成员的偏移为 min(对齐系数,自身长度)的整数倍,不够整数倍的补齐。
2、数据成员为结构体:该数据成员的自身长度为其最大长度的整数倍开始存储
3、整体对齐规则:数据成员按照上述规则对齐之后,其本身也要对齐,
对齐原则是min(对其系数,成员最大长度)的整数倍。
三 :结构体内存分配演练以及在iOS中对象成员的内存分配探索
我们用以下三个结构体做为例子去探索下内存对齐的规则:
struct Struct1 {
char a; // 1 + 7
double b; // 8
int c; // 4
short d; // 2 + 2
} MyStruct1;
struct Struct2 {
int b; // 0 7
char a; // 8
int c; // min(9 4) = 4
short d; // 2
// 16 17
} MyStruct2;
struct Struct3 {
double b; // 0 7
int c; // min(9 4) = 4
char a; // 8
short d; // 2
// 16 17
} MyStruct3;
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSLog(@"%lu-%lu-%lu",sizeof(MyStruct1),sizeof(MyStruct2),sizeof(MyStruct3));
}
}
分别对他们求大小,在Struct1中:
长度 对齐 偏移 区间
char a; 1 0 [0]
double b; 8 8 [8, 15]
int c; 4 16 [16, 19]
short d; 2 20 [20, 21]
1、数据成员的对齐按照#pragma pack-8和自身长度中比较小的那个进行
-- char a 的自身长度为 1, min(1,8) = 1, 按 1 对齐
2、第一个数据成员放在offset为0的地方
-- char a 的偏移为 0
3、整体对齐系数 = min((8,max(int,short,char,double)) = 8,
将 21 提升到 8 的倍数,则为 24,所以最终结果为 24 个字节
在Struct2中:
长度 对齐 偏移 区间
double b; 8 0 [0, 7]
char a; 1 8 [ 8 ]
int c; 4 12 [12, 15]
short d; 2 16 [16, 17]
1、数据成员的对齐按照#pragma pack-8和自身长度中比较小的那个进行
-- double b 的自身长度为 8, min(8,8) = 8, 按 8 对齐
2、第一个数据成员放在offset为0的地方
-- double b 的偏移为 0
3、整体对齐系数 = min((8,max(int,short,char,double)) = 8,
将 17 提升到 8 的倍数,则为 24,所以最终结果为 24 个字节
在Struct3中:
长度 对齐 偏移 区间
double b; 8 0 [0, 7]
int c; 4 8 [8, 11]
char a; 1 12 [12]
short d; 2 14 [14, 15]
1、数据成员的对齐按照#pragma pack-8和自身长度中比较小的那个进行
-- double b 的自身长度为 8, min(8,8) = 8, 按 8 对齐
2、第一个数据成员放在offset为0的地方
-- double b 的偏移为 0
3、整体对齐系数 = min((8,max(int,short,char,double)) = 8,
将 15 提升到 8 的倍数,则为 16,所以最终结果为 16 个字节
最后看下输出结果和我们计算是否一致:
[88992:5534353] 24-24-16
我们可以看到struct2和struct3中相同的数据成员,不同的位置,促成了不同的内存分配结果,其原因就是因为我们的内存对齐规则导致的。
三 :在iOS中类对象的内存分配
我们先看一段代码:
@interface LGTeacher : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) int age;
@property (nonatomic, assign) long height;
@property (nonatomic, strong) NSString *job;
@property (nonatomic, assign) int sex;
@property (nonatomic) char ch1;
@property (nonatomic) char ch2;
@end
Person *person = [[Person alloc] init];
NSLog(@"%lu - %lu",class_getInstanceSize([person class]),malloc_size((__bridge const void *)(person)));
我们创建了一个person对象,并对他的对象实例所占内存大小和系统为此对象开辟空间大小进行打印,得出结果:
[23426:8161973] 40 - 48
为什么对象本身大小和系统为对象分配空间不一致呢?我们根据alloc实现的底层源码知道,对象是以8个字节对齐的,内存优化之后得到结果40我们可以理解,但是为什么系统要为我们多开辟8个字节的空间呢?
我们看下malloc的底层源码实现:
我们跟踪malloc的调用,最后发现这个函数:
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;
}
1: k = (size + NANO_REGIME_QUANTA_SIZE - 1) >> SHIFT_NANO_QUANTUM;
2: slot_bytes = k << SHIFT_NANO_QUANTUM;
我们重点看下这两行代码,第一行我们打印size得到了40(也就是对象的大小),其中这两个宏定义的值NANO_REGIME_QUANTA_SIZE = 16 , SHIFT_NANO_QUANTUM = 4;也就是 k = (40 + 16 - 1) >> 4; 结果右移了4位。然后第二行代码又对上个结果左移了4位。看到这个是不是和alloc的的对象对齐算法很类似?右移之后再左移相当于抹去了二进制的最后四位,前面又加了一个(16-1)得到的结果是16的倍数(也就是16字节对齐,最小大小为16字节)。
得出结论:对象内存的申请按照8字节对齐,不满8字节按照8字节计算;但是实际上malloc实际开辟内存的时候,则是进行了16字节对齐,避免对象之间发生溢出和野指针的问题,所以当对象大小为40时,后面要补8位,最后结果是48