苹果的内存对齐原理

OC底层原理探索文档汇总

讲述苹果的内存是如何进行对齐的,对齐的原理、对齐的算法

主要内容:

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 数据类型占用内存大小

数据类型占用内存大小.png

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

分析:

画图分析.png

结构体3.png

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:为什么我们得到的数据并没有按照对齐原则来排列
如果排列方式会造成大量的内存浪费,苹果会自动进行属性重排,以此来优化性能

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342

推荐阅读更多精彩内容