iOS底层原理--内存对齐

iOS底层原理--alloc&init&new这篇文章中,我们认识到了字节对齐
那么,我们回顾一下什么是字节对齐。

字节对齐

假如一个创建一个对象LGPerson

    //创建LGPerson
     LGPerson *p1 = [LGPerson alloc];

通过调用

//获取size
size = cls->instanceSize(extraBytes);

之后得到size = 16

接下来我们调用class_getInstanceSize(这个方法是获取类对象的实际内存)

// size  = 8
size_t size = class_getInstanceSize([LGPerson class])

最终得到的size = 8

这就验证了字节对齐的存在,而且由于实际大小为8,所以字节对齐的最小值是16个字节。

内存对齐

很多 CPU拒绝读取未对齐数据。当一个程序要求这些 CPU 读取未对齐数据时,这时 CPU 会进入异常处理状态并且通知程序不能继续执行。而且读取未对齐的数据,会大大降低 CPU 的性能。
CPU把内存当成是一块一块的,块的大小可以是2,4,8,16字节大小,因此CPU在读取内存时是一块一块进行读取的。每次内存存取都会产生一个固定的开销,减少内存存取次数将提升程序的性能。所以 CPU 一般会以 2/4/8/16/32 字节为单位来进行存取操作。我们将上述这些存取单位也就是块大小称为(memory access granularity)内存存取粒度。

我们用一段代码来解释内存对齐

  • 首先我们定义2个结构体
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;

接下来用sizeof(strut)打印结构体的内存大小,照理说2个结构体的内容一样,只是排布顺序不同,大小应该一致。那么打印结果如下:

打印结构体.png

明显看到,2个结构体的内存大小不一样,这就是内存对齐产生的影响。

内存对齐的原则

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

内存对齐的解释

上面的话太官方了,我们用比较通俗的语言来解释。

针对上面struct1这个案例来分析,首先我们已经标注出每个成员所占的内存大小。

struct LGStruct1 {
    double a;   //占8位
    char b;     //占1位
    int c;      //占4位
    short d;    //占2位
}struct1;

具体类型在c/oc中所占内存大小如图:


各数据类型所占内存.png

所以按照规则来进行操作:

  • 第⼀个数据成员放在offset为0的地⽅,我们从0开始,double 占8个字节
    double a.png

如上图,a从0开始,长度为8,所以截止到7

  • 每个数据成员存储的起始位置要从该成员⼤⼩或者成员的⼦成员⼤⼩,比如第二个成员为char,char的长度为1,所以从8开始即可

    char b.png

    如上图,b从8开始,长度为1,所以截止到9

  • 还是上面的规则,第三个成员为int,int的长度为4,所以要从4的倍数的位置开始,而邻近的4的倍数的位置为12,所以从12起始

    int c.png

    如上图,c从12开始,长度为4,所以截止到15

  • 第四个成员为short,short的长度为2,所以要从2的倍数的位置开始,而邻近的2的倍数的位置为16,所以从16起始

    short d.png

    如上图,d16开始,长度为2,所以截止到17

  • 最后,结构体的总⼤⼩,也就是sizeof的结果,.必须是其内部最⼤成员的整数倍。不⾜的要补⻬。对于上面的struct1,内部最大成员为a = 8,它的整数倍为8 * 3 = 24

    最终长度.png

所以最终长度为24。

接下来,我们用上面的规则来看下struct2这个例子。

struct LGStruct2 {
    double a;    //占8位
    int b;       //占4位
    char c;      //占1位
    short d;     //占2位
}struct2;

还是通过示意图来来解读。


struct2

最终得出了struct2的长度为16。

结构体的嵌套

接下来,我们来下面这个例子,结构体struct2中嵌套了一个结构体struct1

struct Struct1 {
    double a;  //占8位
    char b;    //占1位
    int c;     //占4位
    short d;   //占2位
}struct1;

struct Struct2 {
    double e;  //占8位
    int f;     //占4位
    char g;    //占1位
    short h;   //占2位
    struct Struct1 I;
}struct2;
  • 首先排列double e,int f,char g,short h,如下图:
    结构体.png
  • 接下来嵌入体,规则为结构体作为成员:如果⼀个结构⾥有某些结构体成员,则结构体成员要从其内部最⼤元素⼤⼩的整数倍地址开始存储。那么针对嵌套的struct1来说,里面最大的元素为double a,长度为8。所以最临近的点为16,所以结构体从16开始。
    结构体.png

    如上图所示,实际需要的内存大小为33个字节。
  • 最后,结构体的总⼤⼩,也就是sizeof的结果,.必须是其内部最⼤成员的整数倍。不⾜的要补⻬。按照最大长度的整数倍,所以计算得出8 * 5 = 40,最终长度为40个字节。
    最终长度.png

所以,再看内存对齐的规则会更加清晰。

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

属性重排(内存优化)

我们知道,所有的oc对象本质上是一个结构体,那么如果按照内存对齐对齐的原则的话,我们一定要特别注意属性存放的位置。因为结构体的大小,和结构体成员的排列顺序有关。
但是实际上,并没有人去关注对象属性的位置。这就是属性重排的作用。
通过一个例子来看。

  • 我们定义一个对象
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface LGPerson : NSObject

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

@property (nonatomic) char c1;
@property (nonatomic) char c2;
@end
NS_ASSUME_NONNULL_END
  • 我们给它赋值
//赋值
LGPerson *person = [LGPerson alloc];
person.name      = @"Cooci";
person.nickName  = @"KC";
person.age       = 18;
person.c1        = 'a';
person.c2        = 'b';
  • 接下来打印数据,po得出LGPerson的内存地址

    po.png

  • 通过x/4gx找出LGPerson的属性地址

    x/4gx.png

  • 我们分别打印4个内存地址


    打印内存地址.png

    分别能得出LGPersonKCCooci。但是77309436513这一串是个乱码

  • 我们把0x0000001200006261拆分成0x00000012,0x62,0x61,分别得到

    image.png

    可以得到18,a,b了。

通过ASCII换算得出97为a,98为b。

我们用一张图来表示


LGPerson.png

对象的内存对齐

我们用一段代码来展示,还是之前的对象,我们来打印一下LGPerson

LGPerson *person = [LGPerson alloc];
person.name      = @"Cooci";
person.nickName  = @"KC";
NSLog(@"%@ - %lu - %lu - %lu",person,sizeof(person),class_getInstanceSize([LGPerson class]),malloc_size((__bridge const void *)(person)));

打印结果如下:


LGPerson.png

对打印信息进行分析

  • NSLog(@"%@", person)
    这个打印出来的结果就是LGPerson,包括它的地址信息。

  • NSLog(@"%lu",sizeof(person))
    person实际上是一个指向LGPerson的指针,一个指针有8个字节,所以大小为8。不管LGPerson有多大,person的大小一直为8。

  • NSLog(@"%lu",class_getInstanceSize([LGPerson class]))
    class_getInstanceSize这个方法的作用就是返回对象真正需要的内存,进行了长度为8的字节对齐。看源码,点击进去,实际走到了这个方法:

//定义WORD_MASK为7UL
#   define WORD_MASK 7UL
static inline uint32_t word_align(uint32_t x) {
    return (x + WORD_MASK) & ~WORD_MASK;
}

跟之前的分析方式一样,传进去的x = 24,WORD_MASK = 7,相加后

// 7 + 31
x + WORD_MASK = 31

转换成二进制

//31的二进制
0000 0000 0001 1111

~WORD_MASK表示WORD_MASK的二进制取反

//7的二进制取反
1111 1111 1111 1000

最后经过与运算,得到

   //31的二进制
   0000 0000 0001 1111
   //7的二进制取反
&1111 1111 1111 1000
   //最终结果
= 0000 0000 0001 1000

可以看到,前面3位都抹0,所以可以得出结论,对象创建的时候,真正进行的是8位的字节对齐

  • NSLog(@"%lu",malloc_size((__bridge const void *)(person)))
    通过malloc算法进行研究

目前已知的16字节内存对齐算法有两种:
alloc源码分析中的align16
malloc源码分析中的segregated_size_to_fit

#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;
}
  • NANO_REGIME_QUANTA_SIZE表示的是1左移4位,得到16
//表示1
0000 0000 0000 0001
//左移4位
0000 0000 0001 0000   = 16
  • size + NANO_REGIME_QUANTA_SIZE - 1,假如size等于40,得出的结果为55
  • >> SHIFT_NANO_QUANTUM表示左移4位,
//55的二进制
0000 0000 0011 0111
//左移4位
0000 0000 0000 0011
  • 然后,k << SHIFT_NANO_QUANTUM,表示右移4位
//k的二进制
0000 0000 0000 0011
//右移4位
0000 0000 0011 0000

可以看到,前面4位都抹0,所以可以得出结论,系统进行内存分配的时候,进行了16位的内存对齐。

结论

虽然我们的结构体遵循内存对齐的原则,但是,它不会随意的浪费内存,会通过内存重排的方式进行结构优化,将多余的内存进行合理的利用,尽最大的可能节省内存。

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