iOS 底层探究:cache_t分析

这是我参与8月更文挑战的第7天,活动详情查看:8月更文挑战算法

在以前的文章中,咱们讲到了NSObject的父类是objc_class,而它包含如下信息数组

Class ISA;
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
复制代码

今天咱们来探索一下cache_t缓存

1.知识准备

1.1数组

数组是用于储存多个相同类型数据的集合。主要有如下优缺点:markdown

  • 优势:访问某个下标的内容很方便,速度快
  • 缺点:数组中进行插入、删除等操做比较繁琐耗时

1.2链表

链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是经过链表中的指针连接次序实现的。主要有如下优缺点:数据结构

  • 优势:插入或者删除某个节点的元素很简单方便
  • 缺点:查找某个位置节点的元素时须要挨个访问,比较耗时

1.3哈希表

哈希表是根据关键码值而直接进行访问的数据结构。主要有如下优缺点:架构

  • 优势:一、访问某个元素速度很快。 二、插入删除操做也很方便
  • 缺点:须要通过一系列运算比较复杂

2.cache的数据结构

类的结构:在objc_class结构体中,由isasuperclasscachebits组成。isasuperclass都是结构体指针,各占8字节。故此,使用内存平移:首地址+16字节,便可探索cache的数据结构体。框架

2.1探索objc源码

找到cache_t的定义less

struct cache_t { 
    private: explicit_atomic<uintptr_t> _bucketsAndMaybeMask; 
    union { 
        struct { 
            explicit_atomic<mask_t> _maybeMask; 
#if __LP64__ 
            uint16_t _flags; 
#endif 
            uint16_t _occupied; 
        }; 
        explicit_atomic<preopt_cache_t *> _originalPreoptCache; 
    }; 
    ... 
};
复制代码
  • _bucketsAndMaybeMask:泛型,传入uintptr_t类型,占8字节
  • union:联合体,包含一个结构体和一个结构体指针_originalPreoptCache
  • struct:包含_maybeMask_flags_occupied三个成员变量,和_originalPreoptCache互斥

咱们找到了cache_t的数据结构,但他的做用还不得而知 经过cache_t的各自方法,能够看出它在围绕bucket_t进行增删改查 找到bucket_t的定义函数

struct bucket_t { 
private: 
    // IMP-first is better for arm64e ptrauth and no worse for arm64. 
    // SEL-first is better for armv7* and i386 and x86_64. 
#if __arm64__ 
    explicit_atomic<uintptr_t> _imp; 
    explicit_atomic<SEL> _sel; 
#else 
    explicit_atomic<SEL> _sel; 
    explicit_atomic<uintptr_t> _imp; 
#endif 
    ... 
};
复制代码
  • bucket_t中包含selimp
  • 不一样架构,selimp的顺序不同

经过selimp不难看出,在cache_t中缓存的应该是方法post

2.2cache_t结构图

image.png

3.cache底层原理

3.1 insert函数

cache_t结构体中,找到insert函数

struct cache_t { 
    ... 
    void insert(SEL sel, IMP imp, id receiver); 
    ... 
};
复制代码

3.2 建立bucket

insert函数,当缓存列表为空时

INIT_CACHE_SIZE_LOG2 = 2, 
INIT_CACHE_SIZE = (1 << INIT_CACHE_SIZE_LOG2), 
mask_t newOccupied = occupied() + 1; 
unsigned oldCapacity = capacity(), capacity = oldCapacity; 
if (slowpath(isConstantEmptyCache())) { 
    // Cache is read-only. Replace it. 
    if (!capacity) capacity = INIT_CACHE_SIZE; 
    reallocate(oldCapacity, capacity, /* freeOld */false); 
}
复制代码
  • newOccupied:已有缓存的大小+1
  • capacity:值为4(1 << 2),缓存列表的初始容量
  • reallocate函数,首次建立,freeOld传入false

reallocate函数,建立buckets存储桶,调用setBucketsAndMask函数

bucket_t *newBuckets = allocateBuckets(newCapacity); 
setBucketsAndMask(newBuckets, newCapacity - 1);
复制代码

setBucketsAndMask函数,不一样架构下代码不同,以当前运行的非真机代码为例

void cache_t::setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask) 
{ 
#ifdef __arm__ 
    // ensure other threads see buckets contents before buckets pointer 
    mega_barrier(); 
    _bucketsAndMaybeMask.store((uintptr_t)newBuckets, memory_order_relaxed); 
    // ensure other threads see new buckets before new mask 
    mega_barrier(); 
    _maybeMask.store(newMask, memory_order_relaxed); 
    _occupied = 0; 
#elif __x86_64__ || i386 
    // ensure other threads see buckets contents before buckets pointer
    _bucketsAndMaybeMask.store((uintptr_t)newBuckets, memory_order_release); 
    // ensure other threads see new buckets before new mask
    _maybeMask.store(newMask, memory_order_release); 
    _occupied = 0; 
#else 
#error Don't know how to do setBucketsAndMask on this architecture. 
#endif 
}
复制代码
  • 传入的newMask为缓存列表的容量-1,用做掩码
  • buckets存储桶,存储到_bucketsAndMaybeMask中。强转uintptr_t类型,只存储结构体指针,即:buckets首地址
  • newMask掩码,存储到_maybeMask
  • _occupied设置为0,由于buckets存储桶目前仍是空的

3.3扩容

若是newOccupied + 1小于等于75%,不须要扩容

#define CACHE_END_MARKER 1 
if (fastpath(newOccupied + CACHE_END_MARKER <= cache_fill_ratio(capacity))) {
    // Cache is less than 3/4 or 7/8 full. Use it as-is. 
} 
// Historical fill ratio of 75% (since the new objc runtime was introduced). 
static inline mask_t cache_fill_ratio(mask_t capacity) { 
    return capacity * 3 / 4; 
}
复制代码
  • CACHE_END_MARKER:系统插入的结束标记,边界做用

超过75%,进行2倍扩容

MAX_CACHE_SIZE_LOG2 = 16, 
MAX_CACHE_SIZE = (1 << MAX_CACHE_SIZE_LOG2), 
capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE; 
if (capacity > MAX_CACHE_SIZE) { 
    capacity = MAX_CACHE_SIZE; 
} 
reallocate(oldCapacity, capacity, true);
复制代码
  • capacity进行2倍扩容,但不能超过65536
  • 调用reallocate函数,扩容时freeOld传入true

reallocate函数,当freeOld传入true

bucket_t *oldBuckets = buckets(); 
bucket_t *newBuckets = allocateBuckets(newCapacity);
setBucketsAndMask(newBuckets, newCapacity - 1); 
if (freeOld) { 
    collect_free(oldBuckets, oldCapacity); 
}
复制代码
  • 建立buckets存储桶,代替原有buckets,新的buckets容量为扩容后的大小
  • 释放原有的buckets
  • 原有buckets中的方法缓存,所有清除

3.4计算下标

insert函数,调用哈希函数,计算sel的下标

mask_t m = capacity - 1; 
mask_t begin = cache_hash(sel, m); 
mask_t i = begin;
复制代码
  • capacity - 1做为哈希函数的掩码,用于计算下标

3.5写入缓存

insert函数,获得buckets存储桶

bucket_t *b = buckets();
复制代码

buckets函数,进行&运算,返回bucket_t类型的结构体脂针,即:buckets首地址

static constexpr uintptr_t bucketsMask = ~0ul; 
struct bucket_t *cache_t::buckets() const 
{ 
    uintptr_t addr = _bucketsAndMaybeMask.load(memory_order_relaxed); 
    return (bucket_t *)(addr & bucketsMask); 
}
复制代码
  • 不一样架构下,bucketsMask的值不同
  • ~0ul0b1111111111111111111111111111111111111111111111111111111111111111
  • &运算:若是两个相应的二进制位都为1,则该位的结果值为1
  • 因此addr & ~0Ul,结果仍是addr

使用下标获取bucket,至关于内存平移。若是bucket中不存在sel,写入缓存

if (fastpath(b[i].sel() == 0)) { 
    incrementOccupied(); 
    b[i].set<Atomic, Encoded>(b, sel, imp, cls()); 
    return; 
}
复制代码
  • incrementOccupied函数,对_occupied进行++
  • set函数,将selimp写入bucket

若是存在sel,而且和当前sel相同,直接return

if (b[i].sel() == sel) { 
    // The entry was added to the cache by some other thread 
    // before we grabbed the cacheUpdateLock. 
    return; 
}
复制代码

不然,表示哈希冲突

3.6 防止哈希冲突

cache_next函数,不一样框架下算法不同,以当前运行的非真机代码为例:

static inline mask_t cache_next(mask_t i, mask_t mask) { 
    return (i+1) & mask; 
}
复制代码
  • 在产生冲突的下标基础上,先进行+1,再和mask进行&运算

do...while中,调用cache_next函数,直到解决哈希冲突为止

do { 
    ... 
} while (fastpath((i = cache_next(i, m)) != begin));
复制代码

结论:

  • capacity:缓存列表的容量
  • occupied:已有缓存的大小
  • maybeMask:使用capacity-1的值做为掩码,在哈希算法、哈希冲突中,用于计算下标
  • 写入缓存时,若是写入缓存后的大小+边界超过容量的75%,进行扩容
    • 扩容:建立新的存储桶,释放原有空间
    • 原有存储桶中的方法缓存所有清除
    • 先进行2倍扩容,再写入缓存
  • 使用哈希函数计算下标,使用下标找到bucket
  • 判断bucket中的sel,不存在则写入
  • 若是存在sel,而且和当前sel相同,直接return
  • 哈希冲突
    • 不一样框架,算法不同
    • 在产生冲突的下标基础上,先进行+1,再和mask进行&运算
    • do...while中,直到解决哈希冲突为止

3.7 为何使用3/4扩容

哈希表具备两个影响其性能的参数:初始容量和负载因子

  • 初始容量时哈希表中存储桶的数量,初始容量知识建立哈希表时的容量
  • 负载因子是在自动增长其哈希表容量以前,容许哈希表得到的满意度的度量

当哈希表中的条目数超过负载因子和当前容量的乘积时,哈希表将会被从新哈希。即:内部数据结构将被重建。所以哈希表的存储桶大约为两倍 负载因子定义为3/4,在时间和空间成本之间提供了一个很好的折中方案

  • 假如负载因子定为1,那么只有当元素填满时才会扩容。虽然能够最大程度的提升空间利用率,可是会增长哈希冲突,所以查询效率会变得低下。因此当加载因子比较大的时候:节省空间资源,增长查找成本
  • 假如负载因子定为0.5,到达空间通常的时候就会去扩容。虽说负载因子比较小能够最大可能的下降哈希冲突,但空间浪费会比较大。因此当加载因子比较小的时候:节省时间资源,耗费空间资源

4 流程图

image.png

相关文章
相关标签/搜索