cache_t探索

285 阅读7分钟

在之前的探索中,我们探索了类的结构中的Class isaClass superclassclass_data_bits_t bits,今天我们就来探索下剩下的这个cache_t cache

还是老样子,先用lldb先来探索一下 c1.png 我们打印的信息和其数据结构一样,但是缓存的内容我们还是不知道内容,从源码中进行探索,查看是否有什么方法可以让我们获取其内部内容:

struct cache_t {
private:
    explicit_atomic<uintptr_t> _bucketsAndMaybeMask;//uintptr_t 就是 unsigned long 8字节
    union { // 联合体 取最大的 8 字节
        struct { // __LP64__ 8字节 !__LP64__ 6字节
            explicit_atomic<mask_t>    _maybeMask; // mask_t 就是 unsigned int 4字节
#if __LP64__
            uint16_t                   _flags; // uint16_t 就是 unsigned short 2字节
#endif
            uint16_t                   _occupied;// uint16_t 就是 unsigned short 2字节
        };
        explicit_atomic<preopt_cache_t *> _originalPreoptCache; // 指针 8字节
    };
……省略部分……
    void incrementOccupied();
    void setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask);
    void reallocate(mask_t oldCapacity, mask_t newCapacity, bool freeOld);
    void collect_free(bucket_t *oldBuckets, mask_t oldCapacity);
    static bucket_t *emptyBuckets();
    static bucket_t *allocateBuckets(mask_t newCapacity);
    static bucket_t *emptyBucketsForCapacity(mask_t capacity, bool allocate = true);
    static struct bucket_t * endMarker(struct bucket_t *b, uint32_t cap);
    void bad_cache(id receiver, SEL sel) __attribute__((noreturn, cold));
public:    
    unsigned capacity() const;
    struct bucket_t *buckets() const;
    Class cls() const;
    void insert(SEL sel, IMP imp, id receiver);
    void copyCacheNolock(objc_imp_cache_entry *buffer, int len);
    void destroy();
    void eraseNolock(const char *func);
#if __LP64__
    bool getBit(uint16_t flags) const {
        return _flags & flags;
    }
    void setBit(uint16_t set) {
        __c11_atomic_fetch_or((_Atomic(uint16_t) *)&_flags, set, __ATOMIC_RELAXED);
    }
    void clearBit(uint16_t clear) {
        __c11_atomic_fetch_and((_Atomic(uint16_t) *)&_flags, ~clear, __ATOMIC_RELAXED);
    }
#endif
……省略部分…….
};

我们发现,在public的方法中有个insert方法,其中传递的参数有SELIMPreceiver,并没有其他明显的取值方法,那我们就先来看看这个insert方法的内部实现。

insert方法

void cache_t::insert(SEL sel, IMP imp, id receiver) {
……省略部分……
    bucket_t *b = buckets();
    mask_t m = capacity - 1;
    mask_t begin = cache_hash(sel, m);
    mask_t i = begin;
    
    // Scan for the first unused slot and insert there.
    // There is guaranteed to be an empty slot.
    do {
        if (fastpath(b[i].sel() == 0)) {
            incrementOccupied();
            b[i].set<Atomic, Encoded>(b, sel, imp, cls());
            return;
        }
        if (b[i].sel() == sel) {
            // The entry was added to the cache by some other thread
            // before we grabbed the cacheUpdateLock.
            return;
        }
    } while (fastpath((i = cache_next(i, m)) != begin));
    bad_cache(receiver, (SEL)sel);
#endif // !DEBUG_TASK_THREADS
}

我们可以看到最下面有个do while循环中将sel、imp设置给了bucket_t类型的变量,查看他的源码:

struct bucket_t {
private:
#if __arm64__
    explicit_atomic<uintptr_t> _imp;
    explicit_atomic<SEL> _sel;
#else
    explicit_atomic<SEL> _sel;
    explicit_atomic<uintptr_t> _imp;
#endif

public:
    inline SEL sel() const { return _sel.load(memory_order_relaxed); }
    inline IMP imp(UNUSED_WITHOUT_PTRAUTH bucket_t *base, Class cls) const {
        uintptr_t imp = _imp.load(memory_order_relaxed);
        if (!imp) return nil;
#if CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_PTRAUTH
        SEL sel = _sel.load(memory_order_relaxed);
        return (IMP)
            ptrauth_auth_and_resign((const void *)imp,
                                    ptrauth_key_process_dependent_code,
                                    modifierForSEL(base, sel, cls),
                                    ptrauth_key_function_pointer, 0);
#elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_ISA_XOR
        return (IMP)(imp ^ (uintptr_t)cls);
#elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_NONE
        return (IMP)imp;
    }
    template <Atomicity, IMPEncoding>
    void set(bucket_t *base, SEL newSel, IMP newImp, Class cls);
};

我们看到他内部的属性只有SELIMP,同时拥有sel()imp(UNUSED_WITHOUT_PTRAUTH bucket_t *base, Class cls),方法获取其内部的值,现在我们先来看一下bucket是怎么存储的sel和imp:

template<Atomicity atomicity, IMPEncoding impEncoding>
void bucket_t::set(bucket_t *base, SEL newSel, IMP newImp, Class cls) {   
    uintptr_t newIMP = (impEncoding == Encoded
                        ? encodeImp(base, newImp, newSel, cls)
                        : (uintptr_t)newImp);
    if (atomicity == Atomic) {
        _imp.store(newIMP, memory_order_relaxed);
        if (_sel.load(memory_order_relaxed) != newSel) {
#ifdef __arm__
            mega_barrier();
            _sel.store(newSel, memory_order_relaxed);
#elif __x86_64__ || __i386__
            _sel.store(newSel, memory_order_release);
        }
    } else {
        _imp.store(newIMP, memory_order_relaxed);
        _sel.store(newSel, memory_order_relaxed);
    }
}

// Sign newImp, with &_imp, newSel, and cls as modifiers.
uintptr_t encodeImp(UNUSED_WITHOUT_PTRAUTH bucket_t *base, IMP newImp, UNUSED_WITHOUT_PTRAUTH SEL newSel, Class cls) const {
        if (!newImp) return 0;
#if CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_PTRAUTH
        return (uintptr_t)
            ptrauth_auth_and_resign(newImp,
                                    ptrauth_key_function_pointer, 0,
                                    ptrauth_key_process_dependent_code,
                                    modifierForSEL(base, newSel, cls));
#elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_ISA_XOR
        return (uintptr_t)newImp ^ (uintptr_t)cls;
#elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_NONE
        return (uintptr_t)newImp;
    }

其用了一个类似之前获取方法属性时用到的entsize_list_tt这样的模版templateAtomicity来判断原子属性,IMPEncoding判断imp是否需要编码,如果需要编码就调用encodeImp,内部其实就是进行签名,然后调用store函数向内存中存入数据。可以看到bucketset就是将selimp写入到内存中,保存我们的方法。

我们在insertdo while之前可以看到bucket插入数据不是按照数组顺序插入的,而是使用cache_hash(sel, m);来获取到插入位置,最大的位置不会超过capacity - 1,我们知道所有hash算法都会出现hash冲突,当发现hash冲突的时候,会使用cache_next(i, m)来移位,如果是inter芯片的mac电脑环境,则直接i+1 &mask,如果是真机环境,则是i ? i-1 : mask

    bucket_t *b = buckets();
    mask_t m = capacity - 1;
    mask_t begin = cache_hash(sel, m);
    mask_t i = begin;
    
static inline mask_t cache_hash(SEL sel, mask_t mask) 
{
    uintptr_t value = (uintptr_t)sel;
#if CONFIG_USE_PREOPT_CACHES
    value ^= value >> 7;
#endif
    return (mask_t)(value & mask);
}

#if CACHE_END_MARKER
static inline mask_t cache_next(mask_t i, mask_t mask) {
    return (i+1) & mask;
}
#elif __arm64__
static inline mask_t cache_next(mask_t i, mask_t mask) {
    return i ? i-1 : mask;
}

如此循环查找buckets中对应下标位置的内存空间,看是否有sel占用位置,如果没有,则直接将数据插入到该位置,并且调用incrementOccupied();使_occupied的值加1。

void cache_t::insert(SEL sel, IMP imp, id receiver) {
    unsigned oldCapacity = capacity(), capacity = oldCapacity;
}
unsigned cache_t::capacity() const {
    return mask() ? mask()+1 : 0; 
}
mask_t cache_t::mask() const {
    return _maybeMask.load(memory_order_relaxed);
}

初始的capacity通过上面的源码我们可以发现实际上就是获取_maybeMask的值,如果不为0,则+1。同样因为之前没有调用方法,所以这里oldCapacity = 0,并且在下面的if-else中会执行reallocate方法中,其中capacity是一个常量,我们可以看到是INIT_CACHE_SIZE,具体值请看整理的宏。再看看reallocate方法

void cache_t::insert(SEL sel, IMP imp, id receiver) {
    if (slowpath(isConstantEmptyCache())) {
        // Cache is read-only. Replace it.
        if (!capacity) capacity = INIT_CACHE_SIZE;
        reallocate(oldCapacity, capacity, /* freeOld */false);
    }
}
void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity, bool freeOld){
    bucket_t *oldBuckets = buckets();
    bucket_t *newBuckets = allocateBuckets(newCapacity);
    setBucketsAndMask(newBuckets, newCapacity - 1);    
    if (freeOld) {
        collect_free(oldBuckets, oldCapacity);
    }
}

void cache_t::setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask) {
#ifdef __arm__
    _bucketsAndMaybeMask.store((uintptr_t)newBuckets, memory_order_relaxed);
    _maybeMask.store(newMask, memory_order_relaxed);
    _occupied = 0;

#elif __x86_64__ || i386
    _bucketsAndMaybeMask.store((uintptr_t)newBuckets, memory_order_release);
    _maybeMask.store(newMask, memory_order_release);
    _occupied = 0;
#else
#error Don't know how to do setBucketsAndMask on this architecture.
#endif
}

很明显,这个方法的作用是初始化一个新的buckets并且在freeOld = true的时候释放旧的buckets,调用了方法就是setBucketsAndMask,继续往里面追踪:

void cache_t::setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask) {
#ifdef __arm__
    _bucketsAndMaybeMask.store((uintptr_t)newBuckets, memory_order_relaxed);
    _maybeMask.store(newMask, memory_order_relaxed);
    _occupied = 0;

#elif __x86_64__ || i386
    _bucketsAndMaybeMask.store((uintptr_t)newBuckets, memory_order_release);
    _maybeMask.store(newMask, memory_order_release);
    _occupied = 0;
#else
#error Don't know how to do setBucketsAndMask on this architecture.
#endif
}

这里是将newBuckets设置进了_bucketsAndMaybeMask,同时将newMask也就是INIT_CACHE_SIZE-1设置进了_maybeMask_occupied = 0很明显这一部分内容就是将我们创建的容器进行初始化,后续得到的capacity就是bucket的长度。到此我们得出了: _occupied实际上就是cache中缓存方法的个数,而_maybeMask就是缓存容器的最大容量。

_bucketsAndMaybeMask

这里简单说一下_bucketsAndMaybeMask,其内部存储的数据会根据架构不同存储的数据也不相同,在 CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED时,_bucketsAndMaybeMask实际上就是一个buckets_t的指针,而在CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS时,_bucketsAndMaybeMask中存在一个混合的数据,其中低48位存储的是buckets_t的指针,而高16位存储的是_maybeMask的值。具体宏含义请查看整理的宏

#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED
    // _bucketsAndMaybeMask is a buckets_t pointer
    // _maybeMask is the buckets mask
    static constexpr uintptr_t bucketsMask = ~0ul;
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
    static constexpr uintptr_t maskShift = 48;
    static constexpr uintptr_t maxMask = ((uintptr_t)1 << (64 - maskShift)) - 1;
    static constexpr uintptr_t bucketsMask = ((uintptr_t)1 << maskShift) - 1;
#if CONFIG_USE_PREOPT_CACHES
    static constexpr uintptr_t preoptBucketsMarker = 1ul;
    static constexpr uintptr_t preoptBucketsMask = bucketsMask & ~preoptBucketsMarker;
#endif

获取存储数据

上面我们知道了selimp都是存在bucket中,那想要获取内部数据我们在cache_t中找到了bucket()方法,我们继续lldb打印一下 c2.png 接下来用我们上面提到的bucket获取内部值的方法来获取selimp c3.png 这不对啊!!按照cache_t中的_occupied = 3说明有缓存方法啊,可是我们获取到的是null啊,为什么?难道找错了?皮一下很开心,我们知道bucket_t不是数组,而是hash表,他的长度是16,sel存储的位置是cache_hash后获取到的,不是顺序的,所以我们不能根据一个位置就来判断是否为空,我们使用之前的地址偏移将bucket的地址偏移再次打印: c4.png c5.png c6.png

我们从偏移1,5,12位的位置找到了缓存的3个方法,那是所有方法都会进行缓存吗?我们加20个方法试一下:

c7.png

我们发现了cache_t中的_occupied的值为4,也就是只存储了4个方法,bucket长度32,我们依次取一个这32个值,看看是否是只存储了4个方法:

c8.png c10.png c9.png

全部打印完毕后,确实只有4个方法,而且可以看出是4个最后调用的方法缓存在了bucket中的,testMethod19,testMethod20是我们自己调用的方法,class是我们使用x/6gx够进行的调用,respondsToSelector是调用class时先调用的验证方法。

扩容

我们第一次运行时bucket容量是16,第二次添加20个方法运行时bucket容量是32,说明当存储空间不够的时候,必然会出现扩容的存在,因此我们看看再次插入的时候,会出现什么情况

void cache_t::insert(SEL sel, IMP imp, id receiver) {
    // Use the cache as-is if until we exceed our expected fill ratio.
    mask_t newOccupied = occupied() + 1;
    unsigned oldCapacity = capacity(), capacity = oldCapacity;
    if (slowpath(isConstantEmptyCache())) {
    } else if (fastpath(newOccupied + CACHE_END_MARKER <= cache_fill_ratio(capacity))) {
        // Cache is less than 3/4 or 7/8 full. Use it as-is.
    }
#if CACHE_ALLOW_FULL_UTILIZATION //arm64 64系统中
    else if (capacity <= FULL_UTILIZATION_CACHE_SIZE && newOccupied + CACHE_END_MARKER <= capacity) {
        // Allow 100% cache utilization for small buckets. Use it as-is.
        // capacity <=8 不会扩容
    }
#endif
    else {
        capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;
        if (capacity > MAX_CACHE_SIZE) {
            capacity = MAX_CACHE_SIZE;
        }
        reallocate(oldCapacity, capacity, true);
    }

我们可以看到,当新插入的数据满足某个条件less than 3/4 or 7/8 full的时候,什么也不做,直接进行插入;否则容量会进行二倍的扩容,并且调用reallocate方法时第三个参数传递的是true,上面初始化时我们也看到了这个方法,当第三个参数为true时,其会调用collect_free(oldBuckets, oldCapacity);释放旧的bucket容器

我们知道capacity是当前bucket的长度,occupied是当前存储的方法数量,那扩容的条件是什么呢?cache_fill_ratio()和 CACHE_END_MARKER会根据使用的机型不同具体的取值也不同,具体值请看整理的宏,总结来说:

  • 在arm64 64位系统(真机/M1电脑)的环境下,newOccupied 小于等于满容量的7/8时,不进行扩容。
  • 在其他系统(inter芯片的mac电脑)上,newOccupied + 1小于等于满容量的3/4时,不进行扩容。

我是inter芯片的mac电脑,所以第一次运行插入第三个方法时,进行了第一次扩容处理,第二次运行进行了第二次扩容处理。

总结

cache_t 内部使用了bucket_t这个容器存储selimp_occupiedcache中缓存方法的个数,_maybeMask是缓存容器的最大容量,_bucketsAndMaybeMask根据架构不同内部存储bucket_maybeMask。同时我们还研究了一下数据插入的流程,当插入的数据大于满容量的3/4(x86_64 / arm64 32位系统) || 7/8(真机 / arm64 64位系统)时,会对容器进行扩容处理,并且将旧的缓存给释放掉。

整理的宏

上面有很多的宏定义,这里我们来整理一下那些宏的含义:

#if defined(__arm64__) && TARGET_OS_IOS && !TARGET_OS_SIMULATOR && !TARGET_OS_MACCATALYST //arm64 真机
#define CONFIG_USE_PREOPT_CACHES 1
#else
#define CONFIG_USE_PREOPT_CACHES 0
#endif


#if CACHE_END_MARKER || (__arm64__ && !__LP64__) // arm64 非64位系统 和 x86_64等
    INIT_CACHE_SIZE_LOG2 = 2,
#else
    INIT_CACHE_SIZE_LOG2 = 1,
#endif
    INIT_CACHE_SIZE      = (1 << INIT_CACHE_SIZE_LOG2),//Inter 1 << 2 4 M1 1 << 1 2
    MAX_CACHE_SIZE_LOG2  = 16,
    MAX_CACHE_SIZE       = (1 << MAX_CACHE_SIZE_LOG2),
};


#if __arm__  ||  __x86_64__  ||  __i386__   // x86_64等
#define CACHE_END_MARKER 1
static inline mask_t cache_fill_ratio(mask_t capacity) {
    return capacity * 3 / 4;
}
#elif __arm64__ && !__LP64__               // arm64 非64位系统
#define CACHE_END_MARKER 0
static inline mask_t cache_fill_ratio(mask_t capacity) {
    return capacity * 3 / 4;
}
#elif __arm64__ && __LP64__               // arm64 64位系统
#define CACHE_END_MARKER 0
static inline mask_t cache_fill_ratio(mask_t capacity) {
    return capacity * 7 / 8;
}
#define CACHE_ALLOW_FULL_UTILIZATION 1
#else
#error unknown architecture
#endif


#define CACHE_MASK_STORAGE_OUTLINED 1
#define CACHE_MASK_STORAGE_HIGH_16 2
#define CACHE_MASK_STORAGE_LOW_4 3
#define CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS 4
#if defined(__arm64__) && __LP64__      // arm64 64位系统
#if TARGET_OS_OSX || TARGET_OS_SIMULATOR // 模拟器 或 M1
#define CACHE_MASK_STORAGE CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
#else                                    // 真机
#define CACHE_MASK_STORAGE CACHE_MASK_STORAGE_HIGH_16
#endif
#elif defined(__arm64__) && !__LP64__  // arm64 32位系统
#define CACHE_MASK_STORAGE CACHE_MASK_STORAGE_LOW_4
#else                                  // x86_64等
#define CACHE_MASK_STORAGE CACHE_MASK_STORAGE_OUTLINED
#endif