2.二刷之内存对齐

目录

1.iOS底层二刷第一课alloc的流程分析

1.为什么要内存对齐

cpu在访问内存时,并不是逐个字节访问的,而是以字长(word size)为单位访问。

比如在iOS中32位系统,字长为4字节,也就是每次CPU访问内存以4字节为一个单位长度。

这么设计的目的,是为了减少CPU访问内存的次数,加大CPU访问内存的吞吐量。比如同样读取8个字节的数据,一次读4个字节只需要读2次。

同样也是因为内存条实际上是切片的这种设计,如下图:

image.png

实际的:

image.png

cpu取数据是按块去读的,如果一位一位的读那么效率会降低非常多;

那么按块读的话,如果不进行内存对齐,当取两个,四个或者八个字节的数据就有可能跨chip。这样cpu就要通过两次寻址找到完整数据,并对数据进行拼接,效率上就损失了好多。因此以空间来置换时间,会进行对齐。

2.iOS中的内存对齐字长

我们在上一篇文章中关于alloc的分析中遇到过对齐的问题。
源码是这样的:

#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
   
uint32_t alignedInstanceSize() const {
    return word_align(unalignedInstanceSize()); 
} 

static inline uint32_t word_align(uint32_t x) {    
    return (x + WORD_MASK) & ~WORD_MASK; 
} 
  • 在iOS中64位系统下,是按照8字节对齐的
  • 在iOS中非64位系统下,是按照4字节对齐的

3.对象影响内存大小的因素有什么

首先来认识一下下面的几个内存相关的方法,方便我们分析内存大小的影响因素。
  • sizeof

sizeofC++中的运算符,作用是返回变量、对象、以及数据类型所占内存的字节数,它返回的大小和系统相关。作用于基本类型,返回基本类型变量的字节大小;作用于自定义类型,返回自定义类型及变量的大小。

  • class_getInstanceSize

Returns the size of instances of a class

这个方法是由runtime提供的获取类的实例所占用的内存大小。

  • malloc_size

/* Returns size of given ptr */

返回传入的指针所指向的内存空间的大小。

搞清楚上面的👆🏻这几个方法,我们分别来通过增加,属性、方法、协议、还有分类,通过打印来观察下对开辟的内存的影响。

  • 属性
@interface LGPerson : NSObject
@property (nonatomic, copy) NSString *name;
@end

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

打印结果:

系统内存开辟分析[56214:11947755] <LGPerson: 0x600000014000> - 8 - 16 - 16

根据打印结果:可以看到

  1. 一个对象类型所占的字节数是8个;
  2. 实例person占用的内存空间大小是16个字节;
  3. person指向的内存空间大小是16个字节;

我们给类LGPerson增加一个属性,然后看看内存的变化,增加了一个

@property (nonatomic, copy) NSString *nickName;

系统内存开辟分析[56367:11960749] <LGPerson: 0x600000205380> - 8 - 24 - 32

发生了变化

占用的内存大小增加了8个,变成了24;person指向的内存空间增加了16变成了32;

继续,我们增加属性

@property (nonatomic, assign) int age;

此时的打印:

系统内存开辟分析[56437:11966024] <LGPerson: 0x600000201ba0> - 8 - 32 - 32

占用的内存大小增加了8个,变成了32;person指向的内存空间和上一次比没变还是32;

先不说这个内存开辟的规律是什么,后面会说这个对齐的规则和原因。我们可以通过这个打印看到,属性是对开辟内存的大小有影响的

  • 方法
    我们继续在类LGPerson中增加方法来看看方法对内存的影响,增加了方法
@interface LGPerson : NSObject

@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *nickName;
@property (nonatomic, assign) int age;
- (void)run;

@end

@implementation LGPerson
- (void)run{
    NSLog(@"niuniu go run!");
}
@end

结果:

系统内存开辟分析[56565:11976327] <LGPerson: 0x6000002055e0> - 8 - 32 - 32

并没有发生变化,我再依次增加了几个方法,或者是类方法,这个打印的大小都不会发生变化。

  • 协议
image.png

也是没有变化的。

  • 分类
image.png

嗯,也没什么变化。

结论

方法、协议、还有分类这几个对对象内存的开辟无影响。

4.内存对齐的规则

  • 数据成员对齐规则:结构或联合的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员存储的起始位置要从该成员大小或者子成员大小的整数倍开始。

    • 例如:int是四个字节,那么要从4的整数倍地址开始存储。
  • 结构体作为成员,如果一个结构体里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储。

    • 例如:struct a里有struct b,b里有char,int,double等元素,那么b要从8的倍数的位置开始存。
  • 结构体总大小,也就是sizeof的结果,必须是其内部最大成员的整数倍。不足要补齐。

基本数据类型占内存大小

image.png
  • 实战演练
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;      //根据规则一跳过13,从14开始 [14,15]
}struct2;
//这里0~15大小本来就为16了,所以不需要补齐了。

5.相关源码探索

我们在上一节课分析alloc的流程里,实际开辟内存空间的代码是:

338d9448c3a84007a2a00444481fe9e4~tplv-k3u1fbpfcp-zoom-in-crop-mark-3024-0-0-0.image.png

是方法calloc方法,它在objc中的只有一个声明,没有具体的实现

void *calloc(size_t __count, size_t __size) __result_use_check __alloc_size(1,2);

它的实现在libmalloc中。继续查看源码来看具体的实现。

  • calloc

它的实现如下:

void * calloc(size_t num_items, size_t size)

{

return _malloc_zone_calloc(default_zone, num_items, size, MZ_POSIX);

}

主要调用的方法是_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;
    if (malloc_check_start) {
        internal_check();
    }
    // Cooci 和谐学习不急不躁 这个流程是有优化的 会和苹果原系统不一定完全重合
    ptr = zone->calloc(zone, num_items, size);

    if (os_unlikely(malloc_instrumented || malloc_check_start ||
                malloc_logger || zone->version < 13)) {
        return _malloc_zone_calloc_instrumented_or_legacy(zone, num_items, size, mzo);
    }
    return zone->calloc(zone, num_items, size);
}

这里核心的调用是calloc,我们进一步点进去进入到它的实现是:

void* (* MALLOC_ZONE_FN_PTR(calloc))(struct _malloc_zone_t *zone, size_t num_items, size_t size); 
/* same as malloc, but block returned is set to zero */

发现没有下面的实现了,我们通过po/p的方式来打印下实际的下一步函数的指向,(我理解的是实际存储的其实就是下一步要跳转到的方法地址)。
来到了(.dylib'default_zone_calloc at malloc.c:504),也就是方法default_zone_calloc

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

我们通过同样的方式,找到了default_zone_calloc中的zone->calloc指向,(.dylib'szone_calloc at magazine_malloc.c:322),找到了szone_calloc,继续跟进,找到方法

void *
szone_calloc(szone_t *szone, size_t num_items, size_t size)
{
    size_t total_bytes;
    if (calloc_get_size(num_items, size, 0, &total_bytes)) {
            return NULL;
    }
    return szone_malloc_should_clear(szone, total_bytes, 1);
}

找到核心方法szone_malloc_should_clear,进一步查看

MALLOC_NOINLINE void *
szone_malloc_should_clear(szone_t *szone, size_t size, boolean_t cleared_requested)
{
    void *ptr;
    msize_t msize;

    if (size <= TINY_LIMIT_THRESHOLD) {
        // size + 15 >> 4 << 4 (size 16字节对齐)
        // (size + (1 << 4) - 1) >> 4 ?????? = (size + 16-1)>> 4
        // #define SHIFT_TINY_QUANTUM 4ull
        msize = TINY_MSIZE_FOR_BYTES(size + TINY_QUANTUM - 1);
        if (!msize) {
            msize = 1;
        }
        // MALLOC_TRACE(TRACE_tiny_malloc, (uintptr_t)rack, TINY_BYTES_FOR_MSIZE(msize), (uintptr_t)tiny_mag_ptr, cleared_requested);
        // #define TINY_BYTES_FOR_MSIZE(_m) ((_m) << SHIFT_TINY_QUANTUM)
        // (size + (1 << 4) - 1) >> 4 ?????? = (size + 16-1)>> 4 << 4
        ptr = tiny_malloc_should_clear(&szone->tiny_rack, msize, cleared_requested);
    } else if (size <= SMALL_LIMIT_THRESHOLD) {
        //(size + 1<<9 -1 )>>9<<9
        msize = SMALL_MSIZE_FOR_BYTES(size + SMALL_QUANTUM - 1);
        if (!msize) {
            msize = 1;
        }
        ptr = small_malloc_should_clear(&szone->small_rack, msize, cleared_requested);
#if CONFIG_MEDIUM_ALLOCATOR
    } else if (szone->is_medium_engaged && size <= MEDIUM_LIMIT_THRESHOLD) {
        msize = MEDIUM_MSIZE_FOR_BYTES(size + MEDIUM_QUANTUM - 1);
        if (!msize) {
            msize = 1;
        }
        ptr = medium_malloc_should_clear(&szone->medium_rack, msize, cleared_requested);
#endif
    } else {
        size_t num_kernel_pages = round_large_page_quanta(size) >> large_vm_page_quanta_shift;
        if (num_kernel_pages == 0) { /* Overflowed */
            ptr = 0;
        } else {
            ptr = large_malloc(szone, num_kernel_pages, 0, cleared_requested);
        }
    }
#if DEBUG_MALLOC
    if (LOG(szone, ptr)) {
        malloc_report(ASL_LEVEL_INFO, "szone_malloc returned %p\n", ptr);
    }
#endif
    /*
     * If requested, scribble on allocated memory.
     */
    if ((szone->debug_flags & MALLOC_DO_SCRIBBLE) && ptr && !cleared_requested && size) {
        memset(ptr, SCRIBBLE_BYTE, szone_size(szone, ptr));
    }

    if (os_unlikely(!ptr)) {
        malloc_set_errno_fast(MZ_POSIX, ENOMEM);
    }

    return ptr;
}

这个分为三种size:tinysmalllarge,我们以small为例进行具体的分析。

if (size <= SMALL_LIMIT_THRESHOLD) {
    // (size + (1<<9) -1 )>>9
        msize = SMALL_MSIZE_FOR_BYTES(size + SMALL_QUANTUM - 1);
        if (!msize) {
                msize = 1;
        }
        // (size +  511 )>>9 << 9
        ptr = small_malloc_should_clear(&szone->small_rack, msize, cleared_requested);
}

前面的SMALL_MSIZE_FOR_BYTES是对齐的一个操作,是512,重点来看方法small_malloc_should_clear

small_malloc_should_clear

加了一小部分的注释,这个方法我也只看懂了一小部分,了解了大概

void *
small_malloc_should_clear(rack_t *rack, msize_t msize, boolean_t cleared_requested)
{
    void *ptr;
    //获取 magazine_t
    mag_index_t mag_index = small_mag_get_thread_index() % rack->num_magazines;
    magazine_t *small_mag_ptr = &(rack->magazines[mag_index]);

    MALLOC_TRACE(TRACE_small_malloc, (uintptr_t)rack, SMALL_BYTES_FOR_MSIZE(msize), (uintptr_t)small_mag_ptr, cleared_requested);

    //加锁
    SZONE_MAGAZINE_PTR_LOCK(small_mag_ptr);

    //如果配置了缓存
#if CONFIG_SMALL_CACHE
    //取上一次释放的空间
    ptr = small_mag_ptr->mag_last_free;

    // 如果上一次刚释放出来的空间 和 传进来的大小正好相同 直接返回
    if (small_mag_ptr->mag_last_free_msize == msize) {
        // we have a winner 很强 很幸运 😊
        small_mag_ptr->mag_last_free = NULL;
        small_mag_ptr->mag_last_free_msize = 0;
        small_mag_ptr->mag_last_free_rgn = NULL;
        SZONE_MAGAZINE_PTR_UNLOCK(small_mag_ptr);
        CHECK(szone, __PRETTY_FUNCTION__);
        if (cleared_requested) {
            memset(ptr, 0, SMALL_BYTES_FOR_MSIZE(msize));
        }
        return ptr;
    }
#endif /* CONFIG_SMALL_CACHE */

    //没开启缓存,直接从small_malloc_from_free_list中读
    while (1) {
        //会匹配到一块和msize大小相同的空间的地址返回
        ptr = small_malloc_from_free_list(rack, small_mag_ptr, mag_index, msize);
        if (ptr) {
            SZONE_MAGAZINE_PTR_UNLOCK(small_mag_ptr);
            CHECK(szone, __PRETTY_FUNCTION__);
            if (cleared_requested) {
                memset(ptr, 0, SMALL_BYTES_FOR_MSIZE(msize));
            }
            return ptr;
        }

#if CONFIG_RECIRC_DEPOT
        //支持存储,再试一次从small_malloc_from_free_list中读
        if (small_get_region_from_depot(rack, small_mag_ptr, mag_index, msize)) {
            ptr = small_malloc_from_free_list(rack, small_mag_ptr, mag_index, msize);
            if (ptr) {
                SZONE_MAGAZINE_PTR_UNLOCK(small_mag_ptr);
                CHECK(szone, __PRETTY_FUNCTION__);
                if (cleared_requested) {
                    memset(ptr, 0, SMALL_BYTES_FOR_MSIZE(msize));
                }
                return ptr;
            }
        }
#endif // CONFIG_RECIRC_DEPOT

        // The magazine is exhausted. A new region (heap) must be allocated to satisfy this call to malloc().
        // The allocation, an mmap() system call, will be performed outside the magazine spin locks by the first
        // thread that suffers the exhaustion. That thread sets "alloc_underway" and enters a critical section.
        // Threads arriving here later are excluded from the critical section, yield the CPU, and then retry the
        // allocation. After some time the magazine is resupplied, the original thread leaves with its allocation,
        // and retry-ing threads succeed in the code just above.
        
        //以上操作都没有读到合适的内存空间,那么需要额外开辟新的heap去满足这次开辟的申请
        if (!small_mag_ptr->alloc_underway) {
            void *fresh_region;

            // time to create a new region (do this outside the magazine lock)
            small_mag_ptr->alloc_underway = TRUE;
            OSMemoryBarrier();
            SZONE_MAGAZINE_PTR_UNLOCK(small_mag_ptr);
            //申请新的page
            fresh_region = mvm_allocate_pages(SMALL_REGION_SIZE,
                    SMALL_BLOCKS_ALIGN,
                    MALLOC_FIX_GUARD_PAGE_FLAGS(rack->debug_flags),
                    VM_MEMORY_MALLOC_SMALL);
            SZONE_MAGAZINE_PTR_LOCK(small_mag_ptr);

            // DTrace USDT Probe
            MAGMALLOC_ALLOCREGION(SMALL_SZONE_FROM_RACK(rack), (int)mag_index, fresh_region, SMALL_REGION_SIZE);

            //内存溢出了 返回空
            if (!fresh_region) { // out of memory!
                small_mag_ptr->alloc_underway = FALSE;
                OSMemoryBarrier();
                SZONE_MAGAZINE_PTR_UNLOCK(small_mag_ptr);
                return NULL;
            }

            region_set_cookie(&REGION_COOKIE_FOR_SMALL_REGION(fresh_region));
            //新申请的page通过hash插入到固定的位置
            // small_malloc_from_region_no_lock 内部包括插入rack_region_insert
            // magazine_t的结构是一个双向链表 ,可以进一步查看下他的数据结构
            //small_malloc_from_region_no_lock 方法中也有对新开辟的空间的双向链表的插入处理 recirc_list_splice_last
            ptr = small_malloc_from_region_no_lock(rack, small_mag_ptr, mag_index, msize, fresh_region);

            // we don't clear because this freshly allocated space is pristine
            small_mag_ptr->alloc_underway = FALSE;
            OSMemoryBarrier();
            SZONE_MAGAZINE_PTR_UNLOCK(small_mag_ptr);
            CHECK(szone, __PRETTY_FUNCTION__);
            return ptr;
        } else {
            SZONE_MAGAZINE_PTR_UNLOCK(small_mag_ptr);
            yield();
            SZONE_MAGAZINE_PTR_LOCK(small_mag_ptr);
        }
    }
    /* NOTREACHED */
}

  • 首先获取了一个 magazine_t,它本身是一个双向链表的结构。

  • 如果支持了缓存策略,会先从它自己的mag_last_free中,读到上一次刚被释放出来的地址,判断和目前申请的msize是不是一样,如果刚好一样,那么!很好,很幸运,直接把这个地址返回。

  • 如果没有那么幸运,那么按部就班让small_malloc_from_free_list处理,也就是从magazine_t->mag_free_list中进行读取空闲的空间,也就是free_list

  • 如果支持循环存储,再试一次从small_malloc_from_free_list中读取一遍。

  • 如果上面一系列操作猛如虎,也都没有找到合适的空间地址分配给msize,那么开辟新的page。

    • 开辟新的page,首先加锁。
    • 判断是否溢出,溢出返回null
    • 没有溢出,开辟新的page成功的话,进行插入操作small_malloc_from_region_no_lock
    • 方法small_malloc_from_region_no_lock内部通过rack_region_insert方法,进行应该是hash计算找到合适的位置进行插入。
    • 并通过recirc_list_splice_last进行新的空间插入到page中,把双向链表的指针指向进行处理。
  • ptr返回给msize,从此msize有了名字,就是ptr的地址~

总结

malloc的过程还是非常严谨的,之所以有这样的过程,依赖于本身的数据结构设计,像magazine_t这种,本身是一个双向链表,内部还存储了自己的free_list,一个magazine_t的结构中带了自己的很多存储信息,开辟信息,上一次释放内容的这样的信息等。

这次对内存对齐的了解比第一次学习的也更加深入了一些,希望再看第三遍,第四遍或者第五遍的时候,能理解的更好。

附上去年写的博客,有个深入的对照:https://www.jianshu.com/p/700833c1140d

参考文章:

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

推荐阅读更多精彩内容