类cache_t的原理分析

类的结构图如下


image.png

之前探索分析了isabits,下面来看看cache,它的偏移是16个字节0x10

一. 通过LLDB打印调用方法之后cache的变化情况

cache_t的成员以及重要的方法:
struct cache_t {
private:
    explicit_atomic _bucketsAndMaybeMask; // 8
    union {
        struct {
            explicit_atomic    _maybeMask; // 4
#if __LP64__
            uint16_t                   _flags;  // 2
#endif
            uint16_t                   _occupied; // 2
        };
        explicit_atomic _originalPreoptCache; // 8
    };
...
public:
    //  获取哈希表首地址,哈希表的大小是: _maybeMask + 1
    struct bucket_t *buckets() const;
    //  插入数据到哈希表
    void insert(SEL sel, IMP imp, id receiver);
...
};
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 _imp;
    explicit_atomic _sel;
#else
    explicit_atomic _sel;
    explicit_atomic _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;
#else
#error Unknown method cache IMP encoding.
#endif
    }
...
};

下面我们通过运行测试代码来看看cache的变化,测试代码如下:

@interface LGPerson : NSObject

@property (nonatomic, copy) NSString *name;
@property (nonatomic) int age;
@property (nonatomic, strong) NSString *hobby;

- (void)saySomething;
- (void)sayHello;

- (void)say1;
- (void)say2;
- (void)say3;
- (void)say4;
- (void)say5;

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {

        LGPerson *p  = [LGPerson alloc];
        Class pClass = [LGPerson class];
        [p say1];
        [p say2];
        [p say3];
        [p say4];
        [p say5];
        
        NSLog(@"%@",pClass);
    }
    return 0;
}

我们先在say1断点停下,输出:
(lldb) p/x pClass
(Class) $0 = 0x0000000100008468 LGPerson
(lldb) p (cache_t*)0x0000000100008478
(cache_t *) $1 = 0x0000000100008478
(lldb) p *$1
(cache_t) $2 = {
  _bucketsAndMaybeMask = {
    std::__1::atomic = {
      Value = 4298515392
    }
  }
   = {
     = {
      _maybeMask = {
        std::__1::atomic = {
          Value = 0
        }
      }
      _flags = 32808
      _occupied = 0
    }
    _originalPreoptCache = {
      std::__1::atomic = {
        Value = 0x0000802800000000
      }
    }
  }
}
(lldb) 

cache为空_maybeMask == 0

继续在say2打断点,输出:
2021-06-27 19:07:12.822270+0800 KCObjcBuild[59475:773940] -[LGPerson say1]
(lldb) p *$1
(cache_t) $3 = {
  _bucketsAndMaybeMask = {
    std::__1::atomic = {
      Value = 4317150992
    }
  }
   = {
     = {
      _maybeMask = {
        std::__1::atomic = {
          Value = 3
        }
      }
      _flags = 32808
      _occupied = 1
    }
    _originalPreoptCache = {
      std::__1::atomic = {
        Value = 0x0001802800000003
      }
    }
  }
}
(lldb) p $3.buckets()[0].sel()
(SEL) $4 = (null)
(lldb) p $3.buckets()[1].sel()
(SEL) $5 = "say1"
(lldb) p $3.buckets()[2].sel()
(SEL) $6 = (null)
(lldb) 

现在哈希表大小为_maybeMask + 1 = 4,有效条目为_occupied = 1,我们打印了三条记录,因为哈希表最后位置保存END_MARKER结束标记

END_MARKER是sel = 1, imp = 哈希表首地址, arm下 imp = 哈希表首地址 - 1 的位置 , arm64下不设置结束标记

继续在say3打断点,输出:
2021-06-27 19:20:33.579398+0800 KCObjcBuild[59475:773940] -[LGPerson say2]
(lldb) p *$1
(cache_t) $7 = {
  _bucketsAndMaybeMask = {
    std::__1::atomic = {
      Value = 4317150992
    }
  }
   = {
     = {
      _maybeMask = {
        std::__1::atomic = {
          Value = 3
        }
      }
      _flags = 32808
      _occupied = 2
    }
    _originalPreoptCache = {
      std::__1::atomic = {
        Value = 0x0002802800000003
      }
    }
  }
}
(lldb) p $3.buckets()[0].sel()
(SEL) $8 = (null)
(lldb) p $3.buckets()[1].sel()
(SEL) $9 = "say1"
(lldb) p $3.buckets()[2].sel()
(SEL) $10 = "say2"
(lldb) 

现在哈希表大小为_maybeMask + 1 = 4,有效条目为_occupied = 2

继续在say4打断点,输出:
2021-06-27 19:23:48.570805+0800 KCObjcBuild[59475:773940] -[LGPerson say3]
(lldb) p *$1
(cache_t) $11 = {
  _bucketsAndMaybeMask = {
    std::__1::atomic = {
      Value = 4311833248
    }
  }
   = {
     = {
      _maybeMask = {
        std::__1::atomic = {
          Value = 7
        }
      }
      _flags = 32808
      _occupied = 1
    }
    _originalPreoptCache = {
      std::__1::atomic = {
        Value = 0x0001802800000007
      }
    }
  }
}
(lldb) p $11.buckets()[0].sel()
(SEL) $12 = (null)
(lldb) p $11.buckets()[1].sel()
(SEL) $13 = (null)
(lldb) p $11.buckets()[2].sel()
(SEL) $14 = (null)
(lldb) p $11.buckets()[3].sel()
(SEL) $15 = "say3"
(lldb) p $11.buckets()[4].sel()
(SEL) $16 = (null)
(lldb) p $11.buckets()[5].sel()
(SEL) $17 = (null)
(lldb) p $11.buckets()[6].sel()
(SEL) $18 = (null)
(lldb) 

现在哈希表大小为_maybeMask + 1 = 8,有效条目为_occupied = 1,哈希表扩容为8say1say2被清除掉,表中只有say3,说明扩容的时候会清除cache

扩容的条件是:如果插入了新条目之后,有效条目数+1,大于哈希表容量的3/4,这就是著名的3/4扩容arm64是7/8扩容
扩容的大小是:当前大小的两倍

继续say5调用处断点,输出:
2021-06-27 19:41:16.561423+0800 KCObjcBuild[59475:773940] -[LGPerson say4]
(lldb) p *$1
(cache_t) $19 = {
  _bucketsAndMaybeMask = {
    std::__1::atomic = {
      Value = 4311833248
    }
  }
   = {
     = {
      _maybeMask = {
        std::__1::atomic = {
          Value = 7
        }
      }
      _flags = 32808
      _occupied = 2
    }
    _originalPreoptCache = {
      std::__1::atomic = {
        Value = 0x0002802800000007
      }
    }
  }
}
(lldb) p $19.buckets()[0].sel()
(SEL) $20 = "say4"
(lldb) p $19.buckets()[1].sel()
(SEL) $21 = (null)
(lldb) p $19.buckets()[2].sel()
(SEL) $22 = (null)
(lldb) p $19.buckets()[3].sel()
(SEL) $23 = "say3"
(lldb) p $19.buckets()[4].sel()
(SEL) $24 = (null)
(lldb) p $19.buckets()[5].sel()
(SEL) $25 = (null)
(lldb) p $19.buckets()[6].sel()
(SEL) $26 = (null)
(lldb) 
最后是调用say5之后,输出:
2021-06-27 19:46:22.875604+0800 KCObjcBuild[59475:773940] -[LGPerson say5]
(lldb) p *$1
(cache_t) $27 = {
  _bucketsAndMaybeMask = {
    std::__1::atomic = {
      Value = 4311833248
    }
  }
   = {
     = {
      _maybeMask = {
        std::__1::atomic = {
          Value = 7
        }
      }
      _flags = 32808
      _occupied = 3
    }
    _originalPreoptCache = {
      std::__1::atomic = {
        Value = 0x0003802800000007
      }
    }
  }
}
(lldb) p $27.buckets()[0].sel()
(SEL) $28 = "say4"
(lldb) p $27.buckets()[1].sel()
(SEL) $29 = (null)
(lldb) p $27.buckets()[2].sel()
(SEL) $30 = (null)
(lldb) p $27.buckets()[3].sel()
(SEL) $31 = "say3"
(lldb) p $27.buckets()[4].sel()
(SEL) $32 = (null)
(lldb) p $27.buckets()[5].sel()
(SEL) $33 = "say5"
(lldb) p $27.buckets()[6].sel()
(SEL) $34 = (null)
(lldb) p $27.buckets()[7].sel()
(SEL) $35 = ""
(lldb) 

二. cache插入代码分析

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; // 1+1
    unsigned oldCapacity = capacity(), capacity = oldCapacity;
    if (slowpath(isConstantEmptyCache())) {  //  第一次进入
        // Cache is read-only. Replace it.
        if (!capacity) capacity = INIT_CACHE_SIZE;//4
        reallocate(oldCapacity, capacity, /* freeOld */false);
    }
    else if (fastpath(newOccupied + CACHE_END_MARKER <= cache_fill_ratio(capacity))) {  //  没有达到扩容条件
        //  arm32,x86_64,i386,不是LP64的arm64,是 3/4 扩容
        //  arm64并且是LP64 是 7/8扩容,最新的iPhone,iPad
        // Cache is less than 3/4 or 7/8 full. Use it as-is.
    }
#if CACHE_ALLOW_FULL_UTILIZATION
    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 {// 4*2 = 8
        //  扩容
        capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;
        if (capacity > MAX_CACHE_SIZE) {
            capacity = MAX_CACHE_SIZE;
        }
        reallocate(oldCapacity, capacity, true);
    }

    bucket_t *b = buckets();
    mask_t m = capacity - 1; // 4-1=3
    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();  //  occupied+1
            b[i].set(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
}

第一次进入给哈希表分配4个条目空间,然后插入

    if (slowpath(isConstantEmptyCache())) {  //  第一次进入
        // Cache is read-only. Replace it.
        if (!capacity) capacity = INIT_CACHE_SIZE;//4
        reallocate(oldCapacity, capacity, /* freeOld */false);
    }

没有达到扩容条件直接进入插入算法。arm32, x86_64,i386和非LP64的arm64,扩容条件是newOccupied + 1 <= 3/4容积;LP64的arm64扩容条件是newOccupied <= 7/8容积

    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.
    }

如果允许满容积扩容,则cache没有满就直接进入插入算法

#if CACHE_ALLOW_FULL_UTILIZATION
    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 {// 4*2 = 8
        //  扩容
        capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;
        if (capacity > MAX_CACHE_SIZE) {
            capacity = MAX_CACHE_SIZE;
        }
        reallocate(oldCapacity, capacity, true);
    }

扩容的代码如下:

ALWAYS_INLINE
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

    ASSERT(newCapacity > 0);
    ASSERT((uintptr_t)(mask_t)(newCapacity-1) == newCapacity-1);

    setBucketsAndMask(newBuckets, newCapacity - 1);
    
    if (freeOld) {
        collect_free(oldBuckets, oldCapacity);
    }
}

#if CACHE_END_MARKER

bucket_t *cache_t::endMarker(struct bucket_t *b, uint32_t cap)
{
    return (bucket_t *)((uintptr_t)b + bytesForCapacity(cap)) - 1;
}

bucket_t *cache_t::allocateBuckets(mask_t newCapacity)
{
    // Allocate one extra bucket to mark the end of the list.
    // This can't overflow mask_t because newCapacity is a power of 2.
    bucket_t *newBuckets = (bucket_t *)calloc(bytesForCapacity(newCapacity), 1);

    bucket_t *end = endMarker(newBuckets, newCapacity);

#if __arm__
    // End marker's sel is 1 and imp points BEFORE the first bucket.
    // This saves an instruction in objc_msgSend.
    end->set(newBuckets, (SEL)(uintptr_t)1, (IMP)(newBuckets - 1), nil);
#else
    // End marker's sel is 1 and imp points to the first bucket.
    end->set(newBuckets, (SEL)(uintptr_t)1, (IMP)newBuckets, nil);
#endif
    
    if (PrintCaches) recordNewCache(newCapacity);

    return newBuckets;
}

#else

bucket_t *cache_t::allocateBuckets(mask_t newCapacity)
{
    if (PrintCaches) recordNewCache(newCapacity);

    return (bucket_t *)calloc(bytesForCapacity(newCapacity), 1);
}

#endif

每次扩容都在表的最后位置设置一个结束标志,sel = 1, imp = 哈希表首地址arm下 imp = 哈希表首地址 - 1 的位置arm64并且是LP64的架构不设置结束标记

    // End marker's sel is 1 and imp points to the first bucket.
    end->set(newBuckets, (SEL)(uintptr_t)1, (IMP)newBuckets, nil);

arm64 && LP64

bucket_t *cache_t::allocateBuckets(mask_t newCapacity)
{
    if (PrintCaches) recordNewCache(newCapacity);

    return (bucket_t *)calloc(bytesForCapacity(newCapacity), 1);
}

扩容的哈希表是一片新分配的内存区,原来的内存区释放,并丢弃之前cache里的所有数据

插入算法是先用selcapacity - 1算出起始哈希位置

    mask_t m = capacity - 1; // 4-1=3
    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 (fastpath(b[i].sel() == 0)) {  //  当前位置为空,可以存储
            incrementOccupied();  //  occupied+1
            b[i].set(b, sel, imp, cls());  //  存入数据
            return;
        }

找不到就用之前的哈希值capacity - 1再做一次哈希,如果不等于起始的哈希位置就继续找空位置

#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;
}
#else
#error unexpected configuration
#endif

arm64的查找方向是i-1向前查找,其他架构是i+1向后查找,我们现在新买的iphone设备都是arm64架构的

补充一点,arm64的CACHE_END_MARKER = 0,而且它的扩容条件是7/8扩容

//  非arm64, mac, 模拟器
#if __arm__  ||  __x86_64__  ||  __i386__ 

// objc_msgSend has few registers available.
// Cache scan increments and wraps at special end-marking bucket.
#define CACHE_END_MARKER 1

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

//  arm64, 但是不是Unix, Linux系统
#elif __arm64__ && !__LP64__

// objc_msgSend has lots of registers available.
// Cache scan decrements. No end marker needed.
#define CACHE_END_MARKER 0

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

//  arm64并且是Unix, Linux系统,最新的iPhone和iPad都是这个架构
#elif __arm64__ && __LP64__

// objc_msgSend has lots of registers available.
// Cache scan decrements. No end marker needed.
#define CACHE_END_MARKER 0

// Allow 87.5% fill ratio in the fast path for all cache sizes.
// Increasing the cache fill ratio reduces the fragmentation and wasted space
// in imp-caches at the cost of potentially increasing the average lookup of
// a selector in imp-caches by increasing collision chains. Another potential
// change is that cache table resizes / resets happen at different moments.
static inline mask_t cache_fill_ratio(mask_t capacity) {
    return capacity * 7 / 8;
}

// Allow 100% cache utilization for smaller cache sizes. This has the same
// advantages and disadvantages as the fill ratio. A very large percentage
// of caches end up with very few entries and the worst case of collision
// chains in small tables is relatively small.
// NOTE: objc_msgSend properly handles a cache lookup with a full cache.
#define CACHE_ALLOW_FULL_UTILIZATION 1

#else
#error unknown architecture
#endif

老的iPhone设备,mac和模拟器都是3/4扩容,新的iPhone设备是7/8扩容

最新的真机扩容是不会设置哈希表结束标记的,只有老的真机,mac,和模拟器会设置

你可能感兴趣的:(类cache_t的原理分析)