The memory structure of cache_t

In class structure and class_datA_bits_t, we analyze class structure, including ISA, superclass, cache, bits. Today we’ll look at the structure of cache_t.

struct objc_class : objc_object {
    // Class ISA;
    Class superclass;
    cache_t cache;             
    class_data_bits_t bits;  
}
Copy the code

If you look at the source code, the underlying structure of cache_t looks like this, but you don’t see anything special here, so if you look down, you see a number of methods associated with bucket_t, look at the structure, and, hey, sel IMP. The class cache is used to cache methods. Called methods are cached in cache_t and can be fetched directly from the cache the next time the method is called.

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;
    };
}
Copy the code
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 }Copy the code

LLDB fetches methods in cache_t

Define a class, call object method, through the source LLDB, outcache_tThe method of caching in.

(lldb) p/x SwwPerson.class (Class) $0 = 0x0000000100008768 SwwPerson (lldb) p (cache_t *)0x0000000100008778 (cache_t *) $1 = 0x0000000100008778 (lldb) p *$1 (cache_t) $2 = { _bucketsAndMaybeMask = { std::__1::atomic<unsigned long> = { Value  = 4302622544 } } = { = { _maybeMask = { std::__1::atomic<unsigned int> = { Value = 3 } } _flags = 32812 _occupied = 1 }  _originalPreoptCache = { std::__1::atomic<preopt_cache_t *> = { Value = 0x0001802c00000003 } } } } (lldb) p $1->buckets() (bucket_t *) $3 = 0x000000010074cf50 (lldb) p *$3 (bucket_t) $4 = { _sel = { std::__1::atomic<objc_selector *> = (null) { Value = nil } } _imp = { std::__1::atomic<unsigned long> = { Value = 0 } } } (lldb) p $1->buckets()[1] (bucket_t) $5 = { _sel = { std::__1::atomic<objc_selector *> = "" { Value = "" } } _imp = { std::__1::atomic<unsigned long> = { Value = 48616 } } } (lldb) p $5.sel (SEL) $6 = "sayHi" Fix-it applied, fixed expression was: $5.sel() (lldb) p $1->buckets()[2]Copy the code

Cache insertion process

If the cache is occupied for the first time: “occupied + 1”; if the cache is initialized for the first time: “occupied + 1”; When you insert the method again, occupied = 2, occupied + 1 <= 3/4, so do nothing. Occupied = 3; capacity*2 = 8; the cached methods will be cleared.

Why 3/4 is chosen as the size of expansion? There is a saying that the load factor, 0.75 is the highest space utilization. And when using hash function to calculate subscripts, it can effectively avoid conflicts and improve efficiency.

if (slowpath(isConstantEmptyCache())) { // Cache is read-only. Replace it. if (! capacity) capacity = INIT_CACHE_SIZE; reallocate(oldCapacity, capacity, /* freeOld */false); 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_END_MARKER = 1 else { capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE; if (capacity > MAX_CACHE_SIZE) { capacity = MAX_CACHE_SIZE; } reallocate(oldCapacity, capacity, true); }Copy the code