iOS底层原理:内存对齐

452 阅读6分钟

一、结构体内存对齐

1、提出问题

思考如下两个结构体 sizeof结果:

如下两个结构体,只是成员位置不同,占用内存大小是否相同呢?

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;


NSLog(@"struct1 sizeof = %lu; struct2 sizeof = %lu",sizeof(struct1),sizeof(struct2));

结果

struct1 sizeof = 24; struct2 sizeof = 16

2、查看规则

2.1、 结构体 内存对齐原则

1:结构(struct)(或联合(union))的数据成员,第⼀个数据成员放在offset为0的地⽅。

2:以后每个数据成员存储的起始位置要从该成员⼤⼩或者成员的⼦成员⼤⼩(只要该成员有⼦成员,⽐如说是数组,结构体等)的整数倍开始。(⽐如int为4字节,则要从4的整数倍地址开始存储)

 对齐值将按照#pragma pack指定的数值和结构(或联合)最大数据成员长度中,比较小的那个进行。
 

3:如果嵌套结构体,嵌套的结构体对齐到自己的最大对齐数(子成员中的最大对齐数)的整数倍处。

4:收尾⼯作:结构体的总⼤⼩,也就是sizeof的结果,为最⼤对齐数的整数倍(每个成员变量都有自己的对齐数)。不⾜的要补⻬。

2.2、 内存占用图

截屏2021-06-08 下午5.45.56.png

3、分析之前的问题

根据内存对齐原则 和 内存占用图 可以分析出 两个结构体分别占用的内存为啥会是 24 和 16

struct LGStruct1 {
    // 第⼀个数据成员放在offset为0的地⽅
    double a;       // 8字节    [0 7]
    // b起始位置要从 1的整数倍 8开始
    char b;         // 1字节    [8]
    // c的起始位置要从 4的整数倍 12开始
    int c;          // 4字节   (9 10 11 [12 13 14 15]
    // d的起始位置应该从 2的整数倍 16开始
    short d;        // 2字节    [16 17] 24
    // 此时共 18 个字节
    // 根据内存对齐原则: 结构体的总大小(sizeof的结果),必须是其内部最大成员的整数倍,不足的要补⻬。
    // 18 不是 8 的整数倍,将内存大小由 18 补齐到 8 的整数倍即为 24
    // sizeof = 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
    // 根据内存对齐原则: 结构体的总大小(sizeof的结果),必须是其内部最大成员的整数倍,不足的要补⻬。
    // 16 是 8 的整数倍
    // sizeof = 16
}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]
    int e;                  // 4  [16,17,18,19]
    struct LGStruct1 str;   // 24 [24,....47]
    // 48 是LGStruct1最大成员double 字节大小8的整数倍 所以 sizeof = 48
}struct3;

NSLog(@"struct3 sizeof = %lu",sizeof(struct3));
结果正如猜测:
struct3 sizeof = 48

4、内存对齐规则的影响因素

在不同架构的处理器下,我们运行同一个示例得到的结果可能不同,甚至不同的编译器配置,也会影响这个结果,我们需要了解影响内存对齐规则的因素有哪些?

4.1、 对齐系数
 1. 通过预编译命令#pragma pack(n) 设定对齐系数
    通过预编译命令#pragma pack()取消自定义字节对齐方式。

每个特定平台上的编译器都有自己的默认“对齐系数”(也叫对齐模数)。程序员可以通过预编译命令#pragma pack(n),n=1,2,4,8,16来改变这一系数,其中的n就是你要指定的“对齐系数”。这里规定的是上界,只影响对齐单元大于n的成员,对于对齐字节不大于n的成员没有影响。

可以认为处理器一次性可以从内存中读/写n个字节。对于大小小于n的成员,按照自己的对齐条件对齐,因为不论怎么放都可以一次性取出。对于对齐条件大于n个字节的成员,成员按照自身的对齐条件对齐和按照n字节对齐需要相同的读取次数,但按照n字节对齐节省空间。

也可以写成

#pragma pack(push,n)

#pragma pack(pop)

eg:

#pragma pack(1) //让编译器对这个结构作1字节对齐

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;
#pragma pack() //取消1字节对齐,恢复为默认

NSLog(@"struct1 sizeof = %lu; struct2 sizeof = %lu",sizeof(struct1),sizeof(struct2));

结果为:

struct1 sizeof = 15; struct2 sizeof = 15

二、类对象的内存对齐

1、问题


@interface LGPerson : NSObject
@property (nonatomic) double a; // 8
@property (nonatomic, assign) int b; // 4
@property (nonatomic) char c; // 1
@property (nonatomic) short d; // 2
@end 
@implementation LGPerson
@end

NSLog(@"申请内存大小为:%lu——-系统开辟内存大小为:%lu", class_getInstanceSize([person class]), malloc_size((__bridge const void *)(person)));

person 申请内存大小为:24——-系统开辟内存大小为:32

思考问题:

1、LGPerson的对象的属性 和上面的结构struct2数据成员一样,为何struct2内存大小为16,而person内存大小和结构struct1的大小24一致.

2、为何person内存大小为24,系统开辟内存大小为32

2、分析

1、首先对象比结构多了isa指针,占8个字节

2、内存中采取了内存对齐的方式,且苹果会进行属性重排来优化内存;

3、class_getInstanceSize:采用8字节对齐,参照的对象的属性内存大小; malloc_size:采用16字节对齐,参照整个对象的内存大小,对象实际分配的内存大小必须是16的整数倍。

2.1、源码分析instanceSize 8字节对齐

上节 alloc探索 我们了解了 alloc 开辟空间的三个核心方法

cls->instanceSize : 计算内存大小 8字节对齐 
(id)calloc(1, size) : 开辟内存,返回地址指针
obj->initInstanceIsa :初始化指针,和类关联起来

instanceSize

    uint32_t alignedInstanceSize() const {
        return word_align(unalignedInstanceSize()); // 8字节对齐
    }

    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;  // 内存大小至少是16个字节
        return size;
    }

objc 源码:--> word_align 8字节对齐: (x + 7) & (7取反)

#ifdef __LP64__
#   define WORD_SHIFT 3UL
#   define WORD_MASK 7UL
#   define WORD_BITS 64
#else
#   define WORD_SHIFT 2UL
#   define WORD_MASK 3UL
#   define WORD_BITS 32
#endif

static inline uint32_t word_align(uint32_t x) {
    return (x + WORD_MASK) & ~WORD_MASK; // 8字节对齐
}
static inline size_t word_align(size_t x) {
    return (x + WORD_MASK) & ~WORD_MASK; // 8字节对齐
}
static inline size_t align16(size_t x) {
    return (x + size_t(15)) & ~size_t(15); // 16字节对齐
}
2.2、malloc 分析

malloc --> 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!
        /*
        k + 15 >> 4 << 4
        
        假定size = 2
        0001 0001 (值:17)
        0000 0000 右移4位
        0001 0000 左移4位  (结果为16)
        */
	return slot_bytes;
}

算法原理:k + 15 >> 4 << 4 ,其中 右移4 + 左移4相当于将后4位抹零,跟 k/16 * 16一样 ,是16字节对齐算法,小于16就成0了

2.3、 汇总目前涉及的内存对齐算法

8字节内存对齐算法

alloc源码分析中的 word_align

16字节内存对齐算法有两种

1)alloc源码分析中的 align16:

2)malloc源码分析中的 segregated_size_to_fit


备注: 参考源码 objc4-818.2; libmalloc-317.40.8;

待继续完善