讲述苹果的内存是如何进行对齐的,对齐的原理、对齐的算法
主要内容:
1、内存对齐原理
2、内存对齐算法
3、苹果的属性重排
1、问题引入
获取内存大小的三种方式
代码:
#import <Foundation/Foundation.h>
#import "LGPerson.h"
#import <objc/runtime.h>
#import <malloc/malloc.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSObject *objc = [[NSObject alloc] init];
NSLog(@"objc对象类型占用的内存大小:%lu",sizeof(objc));
NSLog(@"objc对象实际占用的内存大小:%lu",class_getInstanceSize([objc class]));
NSLog(@"objc对象实际分配的内存大小:%lu",malloc_size((__bridge const void*)(objc)));
}
return 0;
}
运行结果:
2021-10-13 21:18:34.278526+0800 内存对齐原理[58057:3950627] objc对象类型占用的内存大小:8
2021-10-13 21:18:34.278933+0800 内存对齐原理[58057:3950627] objc对象实际占用的内存大小:8
2021-10-13 21:18:34.278971+0800 内存对齐原理[58057:3950627] objc对象实际分配的内存大小:16
分析:
- sizeof()获取的是当前变量或数据的数据类型占用的内存大小
- 包括基本数据类型和引用类型
- 引用类型就是一个指针的大小,也就是8个字节
- 在编译阶段就会确定大小,而不是运行时
- class_getInstanceSize()获取的是一个对象实际占用的内存空间,也就是所有成员变量加起来占用的内存空间
- malloc_size()获取的是系统实际给开辟的内存空间大小
注意一点:这里sizeof获取的是指针大小,也就是objc这个指针变量的大小,因此是8个字节,跟这个对象内部有多少属性没有关系。
2、为什么要进行内存对齐
- 通常内存是由一个个字节组成的,cpu在存取数据时,并不是以字节为单位存储,而是以块为单位存取
- 块的大小为内存存取力度。频繁存取字节未对齐的数据,会极大降低cpu的性能,所以可以通过减少存取次数来降低cpu的开销
3、内存对齐原理
3.1 基本规则
数据成员对齐规则:
struct的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员存储的起始位置要从该成员大小或者成员的子成员大小(只要该成员有子成员,比如嵌套结构体)的整数倍开始。
数据成员为结构体:
如果一个结构体里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储(例如:struct a里面存有struct b,b里面有char、int、double等元素,则b应该从8的整数倍开始存储)
结构体的整体对齐规则:
结构体的总大小,即sizeof的结果,必须是其内部最大成员的整数倍,不足的要补齐
3.2 对规则的理解
规则一:
- 数据成员的对齐规则可以理解为min(m, n) 的公式
- m表示当前成员的开始位置,以0开始,不是以1开始
- n表示当前成员所需要的位数
- 如果m可以整除就可以放在m处存储,也就是m % n == 0。
- 否则就m+1,并继续检查min(m, n),直到可以整除
规则二:
- 当结构体中嵌套了结构体时,结构体成员要从其内部最大元素大小的整数倍地址开始存储
- 比如结构体a嵌套结构体b,b中有char、int、double等,则需要从8的整数倍开始存储
规则三:
- 结构体的内存大小必须是最大成员(包括结构体)内存大小的整数倍,不足的需要补齐
3.3 数据类型占用内存大小
4、具体案例分析
这里分别对结构体、嵌套结构体两种方式的结构体进行字节对齐计算。这里是为了方便,其他的数据类型也是一样的,定义的变量和变量的类型是一致的,只是顺序不一样,但是其所占用的内存大小是不一样的.
4.1 结构体分析
代码:
struct struct1 {
char a; //1字节
double b; //8字节
int c; //4字节
short d; //2字节
}struct1;
struct struct2 {
double b; //8字节
int c; //4字节
short d; //2字节
char a; //1字节
}struct2;
struct struct3 {
double b; //8字节
char a; //1字节
int c; //4字节
short d; //2字节
}struct3;
运行结果:
2021-10-13 22:03:58.227691+0800 内存对齐原理[59377:3979741] struct1=24,struct2=16,struct3=24
分析:
4.2 嵌套结构体分析
代码:
struct Mystruct4 {
int a;
struct Mystruct5 {
double b;
short c;
}struct5;
}struct4;
struct Mystruct6 {
char b;
int c;
short d;
}struct6;
struct Mystruct7 {
int c;
double a;
char b;
struct Mystruct6 struct6;
short d;
}struct7;
运行结果:
2021-10-13 22:13:34.586194+0800 内存对齐原理[59622:3986120] struct4:24,struct7:40
分析:
Mystruct4分析:
- MyStruct4结构体的成员有a和Mystruct5,Mystruct5的成员有b和c,所以Mystruct5的最大成员为8,那么同样的Mystruct4的最大成员也为8
- 变量a:占4字节,从0开始,min(0,4),即 0-3存储a
- 结构体Mystruct5:从4开始,根据内存对齐原则二,即存储开始位置必须是最大的整数倍(最大成员为8),min(4,8)不能整除,继续往后移动,直到8, min(8,8)满足,从8开始存储结构体Mystruct5的变量
- 变量b:占8字节,从8开始,min(8,8),可以整除,即 8-15存储b
- 变量c:占2字节,从16开始,min(16,2),可以整除,即16-17存储c
- 同样因为Mystruct5结构体中必须符合对齐三规则,所以要满足8字节对齐,当前为10个字节,所以需要增加到16字节
- 因此Mystruct4中需要的内存大小是 20字节,根据内存对其原则二,Mystruct4实际的内存大小必须是Mystruct5中最大成员b的整数倍,即必须是8的整数倍,所以sizeof(Mystruct4) 的结果是 24
- 结构体中的结构体的成员在进行存储时也要按照外部的结构体的计数值来进行计算
- 在计算结构体的最大成员时,嵌套的结构体的成员不能相加,而是使用最大的成员拿来比较。
Mystruct7分析:
- struct2的最大成员为4,而double为8,所以struct4的最大成员为8
- struct2要注意字节对齐。
- 外部结构体的字节对齐,内部结构体也要注意字节对齐
- 所以struct2占了10个字节,需要加上30、31成为12个字节。
- 所以d需要保存在32、33位上。也就是总共34个字节,经过对齐后是40个字节
注意:
- 内部结构体的最大成员代表该结构体的最大成员去与外部结构体的其他成员进行比较
- 内部结构体也要保持字节对齐
5、内存对齐算法
算法有两种,本质都是一样,字节对齐本质就是向上取整。
5.1 位与运算
8字节对齐为例说明
代码:
static inline uint32_t word_align(uint32_t x) {
return (x + WORD_MASK) & ~WORD_MASK;
}
计算过程:
WORD_MASK:0000 0111
取反:1111 1000
计算:
1111 1000
这样就以8的倍数向上取整,也就是8字节对齐
简要总结:拿到最后三位为000,其他为111的值,这样就任何数值与他相与都可以将后三位抹零
5.2 位的左移右移
以16字节对齐为例
代码
#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;
}
说明:
k + 15 >> 4 << 4 ,其中 右移4 + 左移4相当于将后4位抹零,跟 k/16 * 16一样 ,是16字节对齐算法,小于16就成0了
简要总结:先右移再左移,右移将后四位抹零,左移恢复其他位数的所在的位置
6 苹果的内存对齐
6.1 实际占用的内存大小按照8字节对齐
【第一步】:class_getInstanceSize
size_t class_getInstanceSize(Class cls)
{
if (!cls) return 0;
return cls->alignedInstanceSize();
}
【第二步】:alignedInstanceSize
uint32_t alignedInstanceSize() const {
return word_align(unalignedInstanceSize());
}
【第三步】:word_align
//第四步,字节对齐计算
/*
# define WORD_MASK 7UL
这里是一个进行字节对齐的算法
WORD_MASK是7,也就是0000 0111,取非后是1111 1000
x就是传入的大小,也就是0000 1111
进行计算后,就是0000 1000
此处可以看出起就是要把后三位抹掉,这就是字节对齐,全都是8的倍数,
*/
//(x + WORD_MASK)是为了向上取整,
//所以说白了,就是以8的倍数向上取整
static inline uint32_t word_align(uint32_t x) {
return (x + WORD_MASK) & ~WORD_MASK;
}
可以看出实际占用的内存大小按照8字节对齐
6.2 实际分配的内存大小是16字节对齐
进入到alloc的底层里查看在创建对象是如何设置开辟的空间大小
先在objc源码中查找到计算开辟空间大小的方法
6.2.1 查找过程
具体过程可以查看另一篇博客,OC对象的本质探索
alloc -> _objc_rootAlloc -> callAlloc -> _objc_rootAllocWithZone -> _class_createInstanceFromZone -> instanceSize
在_class_createInstanceFromZone函数中开辟空间,其中instanceSize函数用来计算开辟空间的大小
6.2.2 查看开辟空间使用多少字节对齐
【第一步】:instanceSize
size_t instanceSize(size_t extraBytes) const {
//进入这里,第一步
if (fastpath(cache.hasFastInstanceSize(extraBytes))) {
return cache.fastInstanceSize(extraBytes);
}
size_t size = alignedInstanceSize() + extraBytes;
// CF requires all objects be at least 16 bytes.
if (size < 16) size = 16;
return size;
}
【第二步】:fastInstanceSize
size_t fastInstanceSize(size_t extra) const
{
ASSERT(hasFastInstanceSize(extra));
if (__builtin_constant_p(extra) && extra == 0) {
return _flags & FAST_CACHE_ALLOC_MASK16;
} else {
size_t size = _flags & FAST_CACHE_ALLOC_MASK;
// remove the FAST_CACHE_ALLOC_DELTA16 that was added
// by setFastInstanceSize
//进行16字节对齐
return align16(size + extra - FAST_CACHE_ALLOC_DELTA16);
}
}
【第三步】:align16
static inline size_t align16(size_t x) {
return (x + size_t(15)) & ~size_t(15);
}
这里可以看出是16字节对齐。
6.3 苹果的重排优化(属性重排)
通过字节对齐的原理和现象发现不同的存储顺序会影响结构体占用内存的大小。
- 如果数据成员根据内存从小到大的顺序排列就更大可能的占用更多的内存
- 如果数据成员根据内存从大到小的顺序排列就更大可能的占用更少的内存
- 简单思考就可以明白因为越小的数据越容易整除,越大的数据越难整除,所以需要从大到小的顺序来排列。
简要总结:苹果会将数据成员根据内存大小从大到小来排序,所以会看起来跟我们的对齐原理不太一样
7、一些问题的回答
问题1:对象实际占用的内存大小和实际分配的内存大小为什么不一样
- 作为属性存储的对象或者其他基本数据类型最大的也就是8个字节,所以实际存储的内存大小8字节对齐已经足够了,采用16字节反而浪费内存
- 如果实际分配内存空间使用8字节,相当于这个对象的isa和其他对象的isa紧挨着,容易造成访问混乱,当无属性时,会预留8字节,即16字节对齐
问题2:二者分别是如何计算的
- 实际占用的内存大小按照8字节对齐,按照8的整数倍向上取整,最小为8个字节
- 实际分配的内存大小按照16字节对齐,按照16的整数倍向上取整,最小为16个字节
问题3:苹果使用什么对齐方式
对于对象来说真正开辟的空间是16字节对齐,其实真正需要的内存大小是8字节对齐,会使用属性重排来优化内存空间。
问题4:为什么我们得到的数据并没有按照对齐原则来排列
如果排列方式会造成大量的内存浪费,苹果会自动进行属性重排,以此来优化性能