内存对齐

内存对齐

源码地址

官方源码 objc4-818.2
https://opensource.apple.com/tarballs/objc4/
可编译源码
https://github.com/LGCooci/objc4_debug
libmalloc 源码
https://opensource.apple.com/tarballs/libmalloc/libmalloc-317.40.8.tar.gz

为什么需要字节对齐?

内存以字节为基本单位,当CPU存取数据时,以块为单位
读取未对齐数据,需要多次访问内存,极大降低CPU的性能
如果数据存储在自然对齐的位置上,可以降低CPU的存取次数。以空间换取时间,提升CPU的访问速率

数据类型的字节占用

类型 32位 64位
bool 1 1
BOOL 1 1
char 1 1
short 2 2
int 4 4
flot 4 4
long 4 8
NSInteger 4(int) 8(long)
CGFloat 4(int) 8(double)
指针 4 8
double 8 8
long long 8

对象数据成员的字节对齐

直接看 objc 源码中 instanceSize 方法

1
2
3
4
5
6
7
8
9
10
inline 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:从缓存中快速获取内存大小
  • alignedInstanceSize:对齐后的对象大小
  • extraBytes:额外字节数,传入的值为0
  • size:不能小于16字节

进入 alignedInstanceSize

1
2
3
uint32_t alignedInstanceSize() const {
return word_align(unalignedInstanceSize());
}
  • word_align: 使用 8 字节对齐
  • unalignedInstanceSize: 未对齐的对象大小

可以看到 8 字节对齐算法 ( + 0x00000111 升阶,再和 0x00000111 取反 )

1
2
3
4
5
# define WORD_MASK 7UL  //  0x00000111

static inline uint32_t word_align(uint32_t x) {
return (x + WORD_MASK) & ~WORD_MASK;
}

为什么是 8 字节对齐?
如果对齐规则大于8字节,会造成内存空间的浪费。如果小于8字节,读取占8字节的数据类型,需要多次访问内存,故此对齐规则为8字节是最好的选择

数据成员对⻬规则

  • 结构(struct)或联合(union)的数据成员,第⼀个数据成员放在offset为0的地⽅,以后每个数据成员存储的起始位置要从该成员⼤⼩或者成员的⼦成员⼤⼩(只要该成员有⼦成员,例如:数组、结构体等)的整数倍开始
    例如:int为4字节,则要从4的整数倍地址开始存储。如果当前开始存储的位置为9,需要空出9、10、11,在12的位置才可存储
  • 结构体作为成员:如果⼀个结构⾥有某些结构体成员,则结构体成员要从其内部最⼤元素⼤⼩的整数倍地址开始存储
    例如:struct a⾥存有struct b,b⾥有char、int、double等元素,那b应该从8的整数倍开始存储
  • 收尾⼯作:结构体的总⼤⼩,也就是sizeof的结果,必须是其内部最⼤成员的整数倍,不⾜的要补⻬

案例

案例1:

1
2
3
4
5
6
7
8
struct Struct1 {
double a; // a占8字节,存储在0~7位置
char b; // b占1字节,存储在8位置。因为8是1的倍数,满足条件
int c; // c占4字节,9~11都不是4的倍数,无法存储,将其空出。所以c存储在12~15位置
short d; // d占2字节,存储在16~17位置
}struct1; // 最后8字节对齐,为24字节

NSLog(@"%lu",sizeof(struct1)); // 结果为 24

案例2:

1
2
3
4
5
6
7
struct Struct2 {
double a; //
int b; // b占4字节,存储在8~11位置
char c; // c占1字节,存储在12位置
short d; // d占2字节,13不是2的倍数,无法存储,将其空出。所以d存储在14~15位置
}struct2; // 最后8字节对齐,为16字节
NSLog(@"%lu",sizeof(struct2)); // 结果为 16

案例3:

1
2
3
4
5
6
7
8
9
10
11
struct Struct3 {
double a; // a占8字节,存储在0~7位置
int b; // b占4字节,存储在8~11位置
char c; // c占1字节,存储在12位置
short d; // d占2字节,13不是2的倍数,无法存储,将其空出。所以d存储在14~15位置
int e; // e占4字节,存储在16~19位置
struct Struct1 str; // str为结构体类型,最大成员占8字节。包含结构体成员,
}struct3; // 从其内部最⼤元素⼤⼩的整数倍地址开始存储。
// 所以str的起始位置为24。str结构体内存对齐后占24字节

NSLog(@"%lu",sizeof(struct2)); // 结果为 48

获取内存大小的三种方式

  • sizeof
    • sizeof不是函数,而是一个操作符
    • 一般会传入基础数据类型,编译器在编译时期即可确定大小
    • sizeof得到的大小,即是该类型占用的空间大小
  • class_getInstanceSize
    • class_getInstanceSize是runtime提供的api
    • 获取实例对象中成员变量的内存大小
  • malloc_size
    • 获取系统实际分配的内存大小

对象开辟空间的内存对齐 (alloc 分配内存大小)

源码分析

因为我们知道获取实际分配的内存大小是通过 malloc_size 函数,所以直接搜索 malloc_size 函数

1
2
3
4
5
6
7
8
9
10
11
12
size_t
malloc_size(const void *ptr)
{
size_t size = 0;

if (!ptr) {
return size;
}

(void)find_registered_zone(ptr, &size);
return size;
}

跟进 find_registered_zone,线索断了

1
2
3
4
...
// 只能找到size相关代码zone->size 并且不能继续跟进
size_t size = zone->size(zone, ptr);
...

查了一些资料,发现内存计算是在 segregated_size_to_fit 方法中进行的,找到该方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#define SHIFT_NANO_QUANTUM		4
#define NANO_REGIME_QUANTA_SIZE (1 << SHIFT_NANO_QUANTUM) // 16 (将1左移4位)

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;
}

size + 15 >> 4 << 4 将 size 右移 4 位,然后左移 4 位。

结论:
对象的内存开辟时 16 进制对齐的

示例

声明一个YCPerson类

1
2
3
4
5
6
7
8
@interface YCPerson : NSObject

@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *nickName;
@property (nonatomic, assign) int age;
@property (nonatomic, assign) long height;

@end

创建这个类对象

1
2
3
4
5
6
7
8
YCPerson *person = [YCPerson alloc];
NSLog(@"sizeof:%lu",sizeof(person));
NSLog(@"class_getInstanceSize:%lu",class_getInstanceSize([YCPerson class]));
NSLog(@"malloc_size:%lu",malloc_size((__bridge const void *)(person)));

// sizeof:8
// class_getInstanceSize:40
// malloc_size:48

sizeof 打印的是 person 指针的大小
class_getInstanceSize 为person实际占用的空间
malloc_size 系统分配的内存大小

注:

  • 对象继承自 NSObject ,内部有isa指针,占用 8 字节
  • 对象会自动对成员变量进行排序,所以对象的成员变量顺序并不会影响对象的大小