[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_t
、bucket_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_hash
是hash
计算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
主要缓存很多的sel
、imp
。
接下来使用lldb
验证一下:
MyPerson *me = [MyPerson alloc];
Class pClass = [MyPerson class];
p/x pClass
读取pClass首地址p (cache_t *)0x0000000100008398
读pClass
的cache
,注意后移16字节(cache
前面有isa
、superclass
、所以需要首地址平移8 + 8 = 16
字节)p *$1
读取cache
里内容p $2.buckets()
获取buckets
里内容
返回来看lldb
打印信息,发现buckets
里面没有值,_sel
,_imp
都为nil
。这是因为我们还没有调用方法,固没有插入缓存。 我们可以lldb
调用函数方法,重新再打印一下buckets
:
细心的话,可以发现,刚才的_occupied
为0,现在是1。那么我们继续打印:
终于可以看到,p $2.buckets()[3]
的输出中,_imp
的Value
不为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的主要内容,如果未能解决你的问题,请参考以下文章