iOS 内存对齐

一、结构体内存对齐

1.1 结构体内存对齐三大原则

  • 数据成员对⻬规则
    结构体(struct)或联合体(union)的数据成员,第一个数据成员放在offset0的地方,以后每个数据成员存储的起始位置要从该成员大小或者成员的子成员大小(只要该成员有子成员,比如说是数组,结构体等)的整数倍开始。(比如int4字节,则要从4的整数倍地址开始存储。)
    min(当前开始的位置m n) m=9 n=4 9 10 11 12

  • 结构体作为成员
    如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储。
    比如struct a中存有struct bbcharintdouble等元素。那b应该从8的整数倍开始存储。

struct structB {
    char c;
    int i;
    double d;
};

struct structA {
    char c;//1
    int i;//4
    struct structB b;//从8开始存储
};
  • 补齐
    结构体的总大小,也就是sizeof的结果,必须是其内部最大成员的整数倍。不足的要补⻬。

1.2 基本数据类型内存大小

C OC 32位 64位
bool BOOL(64位) 1 1
signed char (__signed char)int8_t、BOOL(32位) 1 1
unsigned char Boolean 1 1
short int16_t 2 2
unsigned short unichar 2 2
int 、int32_t NSInteger(32位)、boolean_t(32位) 4 4
unsigned int boolean_t(64位)、NSUInteger(32位) 4 4
long NSInteger(64位) 4 8
unsigned long NSUInteger(64位) 4 8
long long int64_t 8 8
float CGFloat(32位) 4 4
double CGFloat(64位) 8 8

1.3 获取内存大小

1.3.1 sizeof(expression-or-type)

sizeof()C/C++中的关键字,它是一个运算符,不是函数。作用是取得一个对象(数据类型或者数据对象)的长度(即占用内存的大小,以byte为单位,返回size_t)。基本数据类型(intdouble等)的大小与系统相关。结构体涉及字节对齐。
示例:

struct Stu {
    char c;
    int i;
    double d;
};

void test() {
    //基本数据类型
    int age = 18;
    size_t sizeAge1 = sizeof(age);
    size_t sizeAge2 = sizeof age;
    size_t sizeAge3 = sizeof(int);
    NSLog(@"age size: %zu, %zu, %zu",sizeAge1,sizeAge2,sizeAge3);

    //结构体
    struct Stu s;
    s.c = 'c';
    s.i = 18;
    s.d = 180.0;
    
    size_t sizeS1 = sizeof(s);
    size_t sizeS2 = sizeof s;
    size_t sizeS3 = sizeof(struct Stu);
    NSLog(@"s size: %zu, %zu, %zu",sizeS1,sizeS2,sizeS3);

    //指针
    NSObject *obj = [NSObject alloc];
    size_t sizeObj1 = sizeof(obj);
    size_t sizeObj2 = sizeof obj;
    size_t sizeObj3 = sizeof(NSObject *);
    NSLog(@"obj size: %zu, %zu, %zu",sizeObj1,sizeObj2,sizeObj3);
}

输出:

age size: 4, 4, 4
s size: 16, 16, 16
obj size: 8, 8, 8
  • 通过类型和实例都可以获取内存大小,这也说明开辟的内存大小在类型确定后就已经确定了。
  • sizeof是运算符不是函数。3种语法形式都可以,需要注意的是通过类型获取的方式必须在()中。

1.3.2 class_getInstanceSize

这个函数是runtime提供的获取类的实例所占用的内存大小。大小只与成员变量有关。获取的是实际占用的空间(8字节对齐)。
源码如下:

#ifdef __LP64__
#   define WORD_MASK 7UL
#else
#   define WORD_MASK 3UL
#endif

size_t class_getInstanceSize(Class cls)
{
    if (!cls) return 0;
    return cls->alignedInstanceSize();
}

// Class's ivar size rounded up to a pointer-size boundary.
uint32_t alignedInstanceSize() const {
    return word_align(unalignedInstanceSize());
}

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

uint32_t unalignedInstanceSize() const {
    ASSERT(isRealized());
    return data()->ro()->instanceSize;
}

这里就和allocinstanceSize没有命中缓存的逻辑一致了。

1.3.3 malloc_size

malloc_size就是alloc中实际开辟的空间。

1.4 结构体对齐案例

1. 有如下结构体Struct1Struct2分别占用多大内存?

struct Struct1 {
    double a; // [0,7]
    char b;     // [8]
    int c;        // 根据第一准则要从4的倍数开始,所以[12,13,14,15]。跳过9,10,11
    short d;  //[16,17]
}struct1;
//根据第三准则总大小要是8的倍数,那就要分配24字节。

struct Struct2 {
    double a; //[0,7]
    int b;        //[8,11]
    char c;     //[12]
    short d;   //根据准则1跳过13,从14开始 [14,15]
}struct2;
//这里0~15大小本来就为16了,所以不需要补齐了。

验证:

NSLog(@"struct1 size :%zu\nstruct2 size:%zu",sizeof(struct1),sizeof(struct2));

输出:

struct1 size :24
struct2 size:16
  • 结构体中数据类型顺序不一致占用的内存大小可能不一致。
  • 大小计算从0开始,Struct2并没有进行第三原则补齐。

2. 增加一个Struct3中有结构体嵌套,那么占用大小是多少?

struct Struct3 {
    double a;                  //[0,7]
    int b;                         //[8,11]
    char c;                     //[12]
    short d;                   //跳过13 [14,15]
    int e;                        // [16,19]
    struct Struct1 str; //根据准则2,Struct1最大元素为`double`类型,所以从24开始。根据`Struct1`分配的时候24个字节,所以str为[24,47]
}struct3;
//所以Struct3占用内存大小为48字节。

验证:

NSLog(@"struct3 size :%zu",sizeof(struct3));

struct3 size :48

在这里可能有个疑问准则3是先作用在Struct1再作用在Struct3还是最后直接作用在Struct3?不防验证一下:

struct Struct4 {
    struct Struct1 str;
    char c;
}struct4;

Struct1本身占用18字节,补齐后占用24字节。如果Struct4最终占用32字节那么就是第一种情况,占用24字节则是第二种情况。

NSLog(@"struct4 size :%zu",sizeof(struct4));
struct4 size :32

这也就验证了猜想,结构体嵌套从内部开始补齐。这也符合常理。

3. 修改结构体如下:

struct S1 {
    int a; // 4  [0,3]
    char b;// 1  [4]
    short c; // 2 [6,7]
}; // 0~7 8字节

struct S2 {
    double a; // 8 [0,7]
    char b;   // 1 [8]
    struct S1 s1; // 8  [12,19]  按s1自身中存的最大a的4字节的倍数对齐
    bool c; // 1 [20]
};
//0~20一共21个字节,按最大的8字节对齐。应该24字节。
struct S2  s2;
NSLog(@"size :%zu",sizeof(s2));

这个时候s2大小为多少?

分析:

  • S1:
    • int a4个字节,从[0~3]
    • char b1个字节,[4]
    • short c2个字节,需要以2字节对齐,所以跳过5 [6~7]
    • S1整体从0~7不需要补齐。占8字节。
  • S2:
    • double a8字节,[0~7]
    • char b1字节,[8]
    • struct S1 s18字节。由于S1内部最大元素为int a所以需要4倍对齐,所以[12~19]
    • bool c1字节,[20]
    • S2整体从0~21一共21字节,需要按S2中最大元素double a补齐。所以应该是24字节。

输出:

size :24

1.5 对齐原理分析

为什么要根据数据类型跳过部分内存呢?跳过的部分为什么不能存储数据?


内存对齐对比示意图

对于不优化联系存储的情况,CPU读取8~15的内存数据,需要先读取1字节再读取4字节,cpu对于要读取的数据大小是有变化的。而优化后cpu先读取4字节(由于白色3字节空白所以可以直接读取4字节)再读取4字节在这段内存中是没有变化的。相比于第一种优化后cpu的要进行的操作少了,这就实现了通过空间换取时间。

二、系统内存开辟

2.1 案例

HPObject定义如下:

@interface HPObject : NSObject

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

@end

调用:

#import "HPObject.h"
#import <objc/runtime.h>
#import <malloc/malloc.h>

HPObject *hpObj = [HPObject alloc];
hpObj.name = @"HotpotCat";
hpObj.age = 18;

NSLog(@"sizeof:%zu class_getInstanceSize:%zu malloc:%zu",sizeof(hpObj),class_getInstanceSize([HPObject class]),malloc_size((__bridge const void *)(hpObj)));

那么sizeofclass_getInstanceSizemalloc_size分别输出多少呢?
验证:

sizeof:8 class_getInstanceSize:40 malloc:48
  • hpObj是一个结构体指针sizeof返回8
  • class_getInstanceSize由于存在isa8字节对齐所以返回40
  • malloc_size为什么返回48呢?

2.2 malloc_size分析

查看API发现malloc_sizeusr/include中:

image.png

那么就需要源码查看calloc的实现逻辑了。
calloc源码在libmalloc中。
在源码调试中实现如下:

void *p = calloc(1, 40);
NSLog(@"%zu",malloc_size(p));

calloc

void *
calloc(size_t num_items, size_t size)
{
    return _malloc_zone_calloc(default_zone, num_items, size, MZ_POSIX);
}

calloc调用了_malloc_zone_calloc

_malloc_zone_calloc核心实现如下

MALLOC_NOINLINE
static void *
_malloc_zone_calloc(malloc_zone_t *zone, size_t num_items, size_t size,
        malloc_zone_options_t mzo)
{
//……
    void *ptr;
//……
    ptr = zone->calloc(zone, num_items, size);
//……
    return ptr;
}

内部调用了zone->calloc。但是只有calloc的声明没有实现。这个时候有两种方式去找到调用:

  • control + step into跟进去看跳转到了哪里。
  • 直接po/ p zone->calloc查看(有赋值就有存储值)
(lldb) po zone->calloc
(.dylib`default_zone_calloc at malloc.c:385)
(lldb) p zone->calloc
(void *(*)(_malloc_zone_t *, size_t, size_t)) $0 = 0x00000001002e1bb7 (.dylib`default_zone_calloc at malloc.c:385)

default_zone_calloc

static void *
default_zone_calloc(malloc_zone_t *zone, size_t num_items, size_t size)
{
    zone = runtime_default_zone();
    
    return zone->calloc(zone, num_items, size);
}

同理这里的zone->calloc调用到了nano_calloc

nano_calloc

static void *
nano_calloc(nanozone_t *nanozone, size_t num_items, size_t size)
{
    size_t total_bytes;

    //NULL逻辑不用看
    if (calloc_get_size(num_items, size, 0, &total_bytes)) {
        return NULL;
    }

    if (total_bytes <= NANO_MAX_SIZE) {
        //重点逻辑
        void *p = _nano_malloc_check_clear(nanozone, total_bytes, 1);
        if (p) {
            return p;
        } else {
            /* FALLTHROUGH to helper zone */
        }
    }
    //help逻辑大概出错的时候才需要
    malloc_zone_t *zone = (malloc_zone_t *)(nanozone->helper_zone);
    return zone->calloc(zone, 1, total_bytes);
}

得到核心逻辑在_nano_malloc_check_clear中。

_nano_malloc_check_clear

image.png

_nano_malloc_check_clear中计算大小的逻辑是在segregated_size_to_fit中完成的。

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为0的时候直接返回16
        size = NANO_REGIME_QUANTA_SIZE; // Historical behavior
    }
    //(40 + 16 - 1) >> 4 << 4  16字节对齐
    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;
}

16字节对齐,向上对齐。这也就是在系统的内存堆区中对象的内存是16字节对齐,成员变量是以8字节对齐(结构体内部)。对象与对象是16字节对齐。

calloc调用流程

2.3 为什么以16字节对齐?

为什么对象不以8字节对齐?而以16字节对齐?

对象对齐对比

假如一个对象内部成员变量都是8字节大小。

  • 8字节对齐内部没有多余空间,更容易发生访问错误。
  • 16字节对齐内部有多余空间,不容易发生访问错误。

对于64字节的空间:

16 32 48 64
8 16 24 32 40 48 56 64
  • 明显以16字节对齐访问对象和成员变量碰到一起的概率小了。16字节对齐4次,8字节对齐8次。
  • 任何对象都继承自NSObject,但是很少有对象只有一个isa。所以最小的对象都应该是16
  • 如果用32字节对齐呢?很明显空间浪费太大。

三、getInstanceSize与calloc

getInstanceSize的逻辑可以查看上一篇文章alloc流程getInstanceSizecalloc中字节对齐关系如下:

getInstanceSize&calloc内存对齐

  1. instanceSize 正常情况下会走fastInstanceSize分支进行16字节对齐。(setFastInstanceSize_read_images的时候就完成了进行了8字节对齐逻辑,但是没有进行最小16字节兜底)。
  2. instanceSize alignedInstanceSize分支进行了8字节对齐并且有最小16字节兜底。但是没有进行16字节对齐。
  3. calloc中进行了16字节对齐和最小16字节的修正。相当于是对instanceSize的两个分支的兜底。

成员变量字节对齐是8字节对齐,对象的内存对齐是16字节对齐

总结

  • 结构体对齐(三个原则)
    • 3个原则
      • 数据成员对⻬规则:从成员大小或者成员的子成员大小整数倍开始。
      • 结构体作为成员:从内部成员最大元素大小的整数倍地址开始存储。
      • 补齐:必须是内部最大成员的整数倍,不足的要补⻬。
    • 对齐原理:优化CPU读取速度,以空间换时间。
    • 结构体嵌套补齐从内部开始补齐。
  • 内存大小获取
    • sizeof:是运算符,不是函数。获取对象的长度(对象本身)。
    • class_getInstanceSize:获取类的实例所占用的内存大小。大小只与成员变量有关。
    • malloc_sizealloc中实际开辟的空间。
  • calloc 16字节对齐,最小返回16。
    • 最终分配的内存大小逻辑在segregated_size_to_fit中。以16字节对齐向上取整
    • 为什么以16字节对齐?减少访问错误。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,602评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,442评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,878评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,306评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,330评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,071评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,382评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,006评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,512评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,965评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,094评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,732评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,283评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,286评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,512评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,536评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,828评论 2 345

推荐阅读更多精彩内容

  • iOS 内存对其原则 数据成员对⻬规则:结构(struct)(或联合(union))的数据成员,第⼀个数据成员放在...
    Gumball_a45f阅读 245评论 0 0
  • 前言 在iOS底层源码学习中,会需要分析一个结构体所占用的内存大小,这里面就涉及到了内存对齐 今天,我将结合内存对...
    002and001阅读 1,629评论 0 5
  • 首先我们先看一下内存对齐原则: 1.数据成员对⻬规则:结构(struct)(或联合(union))的数据成员,第一...
    e521阅读 423评论 0 0
  • 这篇文章我们来探索一下iOS内存对齐的原理,在探索完内存对齐原理之后,你就会明白内存对齐的好处。 在讲述内存对齐时...
    大橘猪猪侠阅读 809评论 0 3
  • 影响OC对象内存大小的因素 数据类型内存大小: 代码分析 通过class_getInstanceSize获取实例的...
    qinghan阅读 301评论 1 1