[OC学习笔记]class类结构cache_t

Posted Billy Miracle

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[OC学习笔记]class类结构cache_t相关的知识,希望对你有一定的参考价值。

cache_t结构

在objc4源码中,objc_class结构中有一个cache_t的成员变量。

struct objc_class : objc_object 
	...
    // 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的作用是在objc_msgSend过程中会先在cache中根据方法名来通过hash查找方法实现,如果能查找到就直接掉用。如果查找不到然后再去rw_t中查找。然后再在cache中缓存。
cache是一个cache_t类型,我们先看下cache_t底层:

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;
    ;

	...

    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:
    // The following four fields are public for objcdt's use only.
    // objcdt reaches into fields while the process is suspended
    // hence doesn't care for locks and pesky little details like this
    // and can safely use these.
    unsigned capacity() const;
    struct bucket_t *buckets() const;
    Class cls() const;

#if CONFIG_USE_PREOPT_CACHES
    const preopt_cache_t *preopt_cache(bool authenticated = true) const;
#endif

    mask_t occupied() const;
    void initializeToEmpty();

	...

    void insert(SEL sel, IMP imp, id receiver);
    void copyCacheNolock(objc_imp_cache_entry *buffer, int len);
    void destroy();
    void eraseNolock(const char *func);

    static void init();
    static void collectNolock(bool collectALot);
    static size_t bytesForCapacity(uint32_t cap);

	...
;

可以看到有两个成员变量组成。但从定义中看不出成员变量的含义,我们需要结合其中一个方法的实现去了解。因为缓存毕竟是存储某些东西,所以肯定会有插入方法,那么我们从cache_t插入方法入开始进行探索。 往下翻,我们会发现插入方法insert

insert方法

void insert(SEL sel, IMP imp, id receiver);

首先会注意到传入3个参数,方法sel,函数指针imp以及接收者receiver,接下来进入insert方法。

void cache_t::insert(SEL sel, IMP imp, id receiver)

    runtimeLock.assertLocked();

    // Never cache before +initialize is done
    ...

    // Use the cache as-is if until we exceed our expected fill ratio.
    mask_t newOccupied = occupied() + 1;
    unsigned oldCapacity = capacity(), capacity = oldCapacity;
    
    ...

    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


//fastpath:大概率执行
//slowpath:小概率执行

可看到主要是对mask_tbucket_t做些操作。

mask_t newOccupied = occupied() + 1;
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);

b是方法缓存的桶(哈希表)的指针; capacity是目前桶目前的总容量,那么m是桶目前的容量减1,即桶最大的索引。

mask_t begin = cache_hash(sel, m);
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);

cache_hashhash计算index的函数。通过方法名和m计算出index。 最关键的代码是对两者做了与运算,其中mask肯定是2n-1(低位全是1: 0x11、0x1111……),这样相当于是对2n取余了。这样算出的index不会有越界的问题。至于为什么是2n-1,capacity的扩容都是2倍的,初始化的容量也是1左移1位(arm64)或者2位(x86)的值,m = capacity - 1,这个mask就是2n-1了。
对于do while循环:

do 
	// i位置没有存储东西
	if (fastpath(b[i].sel() == 0)) 
		incrementOccupied();//_occupied++;
		// 执行插入操作
		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));

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.
    // explicit_atomic是加了原子性的保护(主要是加个锁)
#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
接下来使用lldb验证一下:

MyPerson *me = [MyPerson alloc];
Class pClass = [MyPerson class];

  • p/x pClass读取pClass首地址
  • p (cache_t *)0x0000000100008398pClasscache,注意后移16字节(cache前面有isasuperclass、所以需要首地址平移8 + 8 = 16字节)
  • p *$1读取cache里内容
  • p $2.buckets()获取buckets里内容

返回来看lldb打印信息,发现buckets里面没有值,_sel_imp都为nil。这是因为我们还没有调用方法,固没有插入缓存。 我们可以lldb调用函数方法,重新再打印一下buckets

细心的话,可以发现,刚才的_occupied为0,现在是1。那么我们继续打印:

终于可以看到,p $2.buckets()[3]的输出中,_impValue不为0,也就是说,buckets有内容了。

insert方法中的扩容

在源码里,往前面提到的那个do while循环前面看,可以发现一段检查capacity(容量)的代码,仔细看,可以看到这里进行了有关扩容的操作:

// 新的占用是原来占用量+1
mask_t newOccupied = occupied() + 1;
unsigned oldCapacity = capacity(), capacity = oldCapacity;
// 没有初始化
if (slowpath(isConstantEmptyCache())) 
	// Cache is read-only. Replace it.
	// 容量变成 初始化容量
	//INIT_CACHE_SIZE_LOG2 = 2
	//INIT_CACHE_SIZE      = (1 << INIT_CACHE_SIZE_LOG2)
    if (!capacity) capacity = INIT_CACHE_SIZE;
    // 重新开辟cache内存空间
    reallocate(oldCapacity, capacity, /* freeOld */false);

//CACHE_END_MARKER = 1
// 如果加1还能容下
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.

//arm64
#if CACHE_ALLOW_FULL_UTILIZATION
//FULL_UTILIZATION_CACHE_SIZE_LOG2 = 3,
//FULL_UTILIZATION_CACHE_SIZE = (1 << FULL_UTILIZATION_CACHE_SIZE_LOG2)
//FULL_UTILIZATION_CACHE_SIZE = 8
else if (capacity <= FULL_UTILIZATION_CACHE_SIZE && newOccupied + CACHE_END_MARKER <= capacity) 
    // Allow 100% cache utilization for small buckets. Use it as-is.

#endif
//需要扩容
else 
    //容量变成以前2倍,但不超过MAX_CACHE_SIZE:2的16次方
    //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);

通过注释大家应该也能大致了解整个过程了。
小结

arm64结构下,当目前缓存的大小+1小于等于桶子的大小的7/8的时候不扩容,当桶子的大小小于等于8,并且目前缓存的大小+1小于等于桶子的大小的时候也不扩容(桶子小于8的时候存满了才扩容)。
x86_64结构下,当目前缓存的大小+1,再+1小于等于桶子大小的3/4的时候不扩容。

接下来看一下reallocate (重新开辟空间)方法源码:

void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity, bool freeOld)

    // 第一次开辟、扩容之后的都会走这里
    // 设置旧桶, 初始化新桶
    bucket_t *oldBuckets = buckets();
    bucket_t *newBuckets = allocateBuckets(newCapacity);

    // Cache's old contents are not propagated. 
    // This is thought to save cache memory at the cost of extra cache fills.
    // fixme re-measure this
    // 缓存的旧内容不会传播。
    // 这被认为是以额外的缓存填充为代价来节省缓存内存。
    // fixme 重新测量此值
    ASSERT(newCapacity > 0);
    ASSERT((uintptr_t)(mask_t)(newCapacity-1) == newCapacity-1);
    
    // 给cache_t 第一个参数赋值
    // explicit_atomic<uintptr_t> _bucketsAndMaybeMask;
    // 第一个参数即存储buckets和MaybeMask
    // setBucketsAndMask主要设置新值释放旧值, 并且将_occupied设置为0
    setBucketsAndMask(newBuckets, newCapacity - 1);
    // freeOld: 第一次false, 扩容为true
    if (freeOld) 
        // 如果有原始的脏内存会做一次清空, 下面可以看详细源码
        // 扩容之后,里面之前数据都没有了
        collect_free(oldBuckets, oldCapacity);
    

接下来看一下里边的setBucketsAndMask()方法:

void cache_t::setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask)

    // objc_msgSend uses mask and buckets with no locks.
    // It is safe for objc_msgSend to see new buckets but old mask.
    // (It will get a cache miss but not overrun the buckets' bounds).
    // It is unsafe for objc_msgSend to see old buckets and new mask.
    // Therefore we write new buckets, wait a lot, then write new mask.
    // objc_msgSend reads mask first, then buckets.
    
    // objc_msgSend使用mask和buckets时没有锁。
    // 对于objc_msgSend来说,看到新的buckets但旧的mask是安全的。(它会得到一个缓存未命中,但不会超过buckets'的界限)。
    // objc_msgSend看到旧的buckets和新mask是不安全的。
    // 因此,我们写新的buckets,等待很多,然后写新的mask。
    // objc_msgSend先读mask,然后读buckets。
#ifdef __arm__
    // ensure other threads see buckets contents before buckets pointer
    // arm真机环境设置屏障, 保证 后面执行bucketsAndMaybeMask存储安全
    mega_barrier();
    
    // _bucketsAndMaybeMask存储新值, 释放旧值
    _bucketsAndMaybeMask.store((uintptr_t)newBuckets, memory_order_relaxed);

    // ensure other threads see new buckets before new mask
    // 结束屏障
    mega_barrier();
    
    // _maybeMask存储新值, 释放旧值
    _maybeMask.store(newMask, memory_order_relaxed);
    // 占位occupied 设置为0, 新桶时又把_occupied设置为0
    _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存储新值, 释放旧值
    _maybeMask.store(newMask, memory_order_release);
    //  占位occupied 设置为0, 新桶时又把_occupied设置为0
    _occupied = 0;
#else
#error Don't know how to do setBucketsAndMask on this architecture.
#endif

看一下store()

void store(T desired, std::memory_order order) noexcept 
	std::atomic<T>::store(desired, order);

再看一下collect_free

void cache_t::collect_free(bucket_t *data, mask_t capacity)

#if CONFIG_USE_CACHE_LOCK
    cacheUpdateLock.assertLocked();
#else
    runtimeLock.assertLocked();
#endif

    if (PrintCaches) recordDeadCache(capacity);
    // 直接掉底层清空, 回收操作
    _garbage_make_room ();
    garbage_byte_size += cache_t::bytesForCapacity(capacity);
    garbage_refs[garbage_count++] = data;
    cache_t::collectNolock(false);

思考:

在扩容的时候,苹果为什么要释放旧的缓存,而不是把旧的放入到新的缓存中呢?

  • 提高msgSend效率,扩容是发生在msgSend中,如果再做copy操作,会影响消息发送的效率。
  • 缓存命中概率,每个方法调用的概率在底层设计的时候,都视为是一样的。所以之前缓存的方法,在后面调用的概率和其他方法的概率是一样的。即清除之前的缓存,不会影响命中概率。
  • 减少扩容次数,从而提高效率。还是2的衍生,如果及时清除,可以缓存更多的方法,这样,扩容的概率跟放入新缓存相比更小。

以上是关于[OC学习笔记]class类结构cache_t的主要内容,如果未能解决你的问题,请参考以下文章

ojective-C学习笔记关于面向对象编程

OC 类的结构分析

Java虚拟机学习笔记——类文件结构与字节码指令

[OC学习笔记]Block三种类型

[OC学习笔记]熟悉Objective-C

深入理解JVM学习笔记——-6类文件结构