06-OC方法缓存机制cache_t

简介: 06-OC方法缓存机制cache_t

cache_t

回顾objc_object的结构,内部有个cache_t,主要是用作方法缓存,是用哈希表来缓存调用过的方法,主要用来提高方法调用过程中的查找速度;

objc_object的结构如下:

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
    class_rw_t *data() const {
        return bits.data();
    }
}

cache_t的结构如下:

struct cache_t {
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
    explicit_atomic<uintptr_t> _maskAndBuckets;
    mask_t _mask_unused;
    static constexpr uintptr_t maskShift = 48;
    static constexpr uintptr_t maskZeroBits = 4;
    static constexpr uintptr_t maxMask = ((uintptr_t)1 << (64 - maskShift)) - 1;
    static constexpr uintptr_t bucketsMask = ((uintptr_t)1 << (maskShift - maskZeroBits)) - 1;
    static_assert(bucketsMask >= MACH_VM_MAX_ADDRESS, "Bucket field doesn't have enough bits for arbitrary pointers.");
#if __LP64__
    uint16_t _flags;
#endif
    uint16_t _occupied;
public:
    static bucket_t *emptyBuckets();
    struct bucket_t *buckets();
    mask_t mask();
    mask_t occupied();
    void incrementOccupied();
    void setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask);
    void initializeToEmpty();
    unsigned capacity();
    bool isConstantEmptyCache();
    bool canBeFreed();
#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
    static size_t bytesForCapacity(uint32_t cap);
    static struct bucket_t * endMarker(struct bucket_t *b, uint32_t cap);
    void reallocate(mask_t oldCapacity, mask_t newCapacity, bool freeOld);
    void insert(Class cls, SEL sel, IMP imp, id receiver);
    static void bad_cache(id receiver, SEL sel, Class isa) __attribute__((noreturn, cold));
    ...
}

其中buckets为bucket_t 结构体类型的指针,bucket_t的结构如下:

struct bucket_t {
private:
    explicit_atomic<uintptr_t> _imp;
    explicit_atomic<SEL> _sel;
    uintptr_t modifierForSEL(SEL newSel, Class cls) const {
        return (uintptr_t)&_imp ^ (uintptr_t)newSel ^ (uintptr_t)cls;
    }
public:
    inline SEL sel() const { return _sel.load(memory_order::memory_order_relaxed); }
    inline IMP imp(Class cls) const {
        ...
    }
    void set(SEL newSel, IMP newImp, Class cls);
    ...
}

存储的时候_sel作为key,_imp作为value,存入到哈希表中;当调用方法进行查找的时候直接利用sel作为key,来查找对应的实现imp;

根据源码,我们来自己构造一下这种结构,来探究一下内部到底是怎么存储的?测试代码如下:

typedef uint32_t mask_t;
struct glt_bucket_t {
    SEL _sel;
    IMP _imp;
};
struct glt_cache_t {
    struct glt_bucket_t *_buckets;
    mask_t _mask;
    uint16_t _flags;
    uint16_t _occupied;
};
struct glt_objc_class {
    Class ISA;
    Class superClass;
    struct glt_cache_t cache;
};
@interface Person : NSObject
- (void)test1;
- (void)test2;
- (void)test3;
@end
@implementation Person
- (void)test1 { }
- (void)test2 { }
- (void)test3 { }
@end
void log(Person *person) {
    struct glt_objc_class *personClass = (__bridge struct glt_objc_class *)[person class];
    glt_cache_t cache = personClass->cache;
    glt_bucket_t *buckets = cache._buckets;
    NSLog(@"_mask = %d, _occupied = %d", cache._mask, cache._occupied);
    for (int i = 0; i < cache._mask; i++) {
        glt_bucket_t bucket = buckets[i];
        NSLog(@"方法缓存: %@", NSStringFromSelector(bucket._sel));
    }
}
int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
        Person *person = [[Person alloc] init];
        [person test1];
        log(person);
        [person test2];
        [person test3];
        log(person);
    }
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}

运行结果:

_mask = 3, _occupied = 2
方法缓存: init
方法缓存: test1
方法缓存: (null)
_mask = 7, _occupied = 2
方法缓存: test3
方法缓存: (null)
方法缓存: test2
方法缓存: (null)
方法缓存: (null)
方法缓存: (null)
方法缓存: (null)

根据log我们来探究一下_mask、_occupied值为什么会变化?以及在第二个log中init和test1方法为什么没有打印出来?通过源码分析,会发现调用到了下面这样一个函数:

void cache_t::insert(Class cls, SEL sel, IMP imp, id receiver)
{
    mask_t newOccupied = occupied() + 1;
    unsigned oldCapacity = capacity(), capacity = oldCapacity;
    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 <= capacity / 4 * 3)) {
        // Cache小于等于 3/4,此处不做处理
    }
    else {//扩容为原来容量的2倍,即4\8\16...
        capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;
        if (capacity > MAX_CACHE_SIZE) {
            capacity = MAX_CACHE_SIZE;
        }
        //此处会释放掉所有的内容,清空buckets,把存储的内容都设置为null
        reallocate(oldCapacity, capacity, true);
    }
    bucket_t *b = buckets();
    mask_t m = capacity - 1;
    mask_t begin = cache_hash(sel, m);
    mask_t i = begin;
    do {
        //当前缓存中没有找到sel,则直接存储
        if (fastpath(b[i].sel() == 0)) {
            incrementOccupied(); //_occupied++ 增加计数
            b[i].set<Atomic, Encoded>(sel, imp, cls);
            return;
        }
        //说明已经插入了无需再次插入
        if (b[i].sel() == sel) {
            return;
        }
        //cache_next:说明这个位置已经被占用了、重新寻找下一个位置
        //寻找的方式就是进行arm64下是进行-1操作,如果减到0了则直接等于mask
    } while (fastpath((i = cache_next(i, m)) != begin));
    cache_t::bad_cache(receiver, (SEL)sel, cls);
}

通过源码我们可以发现:capacity保存了当前的总容量,并且初始的容量为4,也就是哈希表的总长度,如果总容量小于等于3/4不做处理,否则的话会进行扩容,容量会扩充为原来的2倍(4、8、16...),并且在扩容的时候会调用reallocate函数,在其内部会释放掉原来存储的内容,故上面的init方法和test1方法没有打印出来,此外在其内部也会对_mask进行赋值,值为capacity-1;

接下来会插入_sel和_imp,具体步骤为:

1. 先查看_sel & mask后的结果,也就是和begain对比要插入的位置,得到的索引可能会相等(哈希冲突),如果相等,此处解决方式为将值-1后再进行查找,直到找到不相等的再进行插入;

static inline mask_t cache_next(mask_t i, mask_t mask) {
    return i ? i-1 : mask;
}

2. 接着查看哈希表中是否存储即将要插入的_sel,如果没有,则直接将_sel作为key,_imp作为value插入到hash表中,并把_occupied的值+1;

3. 如果已经插入过了即b[i].sel() == sel,则不再插入;

总结

1.  _mask和_occupied分别是什么?有什么作用?

_mask的值为哈希表总长度减1,主要是利用_sel & _mask来计算新的_sel要插入的位置,如果存在哈希冲突(也就是&出的结果一致),在arm64下会将递归遍历&后的计算结果使其-1,如果为0则等于mask,直到找到为止;

_occupied主要是用来存储已经缓存的方法数量;

2. bucket数据为什么会为空?

因为是在扩容的时候(4,8,16,...),会清除旧的数据,重新分配内存;

3. cache_t中buckets与_mask、_occupied、_sel、_imp的关系?

buckets中有多个bucket_t,_mask为哈希表的总长度-1,_occupied为已经缓存的方法数量,哈希表存储的时候_sel作为key, _imp作为value进行存储;本文部分内容来可能来源于网络,发布的内容如果侵犯了您的权益,请联系我们尽快删除!

相关文章
|
3月前
|
缓存 监控 Linux
Linux系统清理缓存(buff/cache)的有效方法。
总结而言,在大多数情形下你不必担心Linux中buffer与cache占用过多内存在影响到其他程序运行;因为当程序请求更多内存在没有足够可用资源时,Linux会自行调整其占有量。只有当你明确知道当前环境与需求并希望立即回收这部分资源给即将运行重负载任务之前才考虑上述方法去主动干预。
1169 10
|
7月前
|
缓存 并行计算 PyTorch
PyTorch CUDA内存管理优化:深度理解GPU资源分配与缓存机制
本文深入探讨了PyTorch中GPU内存管理的核心机制,特别是CUDA缓存分配器的作用与优化策略。文章分析了常见的“CUDA out of memory”问题及其成因,并通过实际案例(如Llama 1B模型训练)展示了内存分配模式。PyTorch的缓存分配器通过内存池化、延迟释放和碎片化优化等技术,显著提升了内存使用效率,减少了系统调用开销。此外,文章还介绍了高级优化方法,包括混合精度训练、梯度检查点技术及自定义内存分配器配置。这些策略有助于开发者在有限硬件资源下实现更高性能的深度学习模型训练与推理。
1186 0
|
3月前
|
缓存 监控 Ubuntu
Ubuntu操作系统下清除系统缓存与无用文件的方法
通过上述步骤断行综合性地对Ubuntu进行优化与整洁可显著改善其性能表现及响应速度。然而,请注意在执行某些操作前确保充分了解其潜在影响;例如,在移除旧内核之前确认新内核稳定运行无问题;而对于关键配置更改则需确保备份好相关设置以便恢复原状态。
437 0
|
10月前
|
缓存 NoSQL Java
springboot怎么使用rides缓存方法的返回值 完整例子
通过上述步骤,我们成功地在 Spring Boot 项目中集成了 Redis 缓存,并通过注解的方式实现了方法返回值的缓存。这种方式不仅提高了系统的性能,还简化了缓存管理的复杂度。使用 Spring Boot 的缓存注解和 Redis,可以轻松地实现高效、可靠的缓存机制。
190 23
|
9月前
|
存储 缓存 分布式计算
【赵渝强老师】Spark RDD的缓存机制
Spark RDD通过`persist`或`cache`方法可将计算结果缓存,但并非立即生效,而是在触发action时才缓存到内存中供重用。`cache`方法实际调用了`persist(StorageLevel.MEMORY_ONLY)`。RDD缓存可能因内存不足被删除,建议结合检查点机制保证容错。示例中,读取大文件并多次调用`count`,使用缓存后执行效率显著提升,最后一次计算仅耗时98ms。
203 0
【赵渝强老师】Spark RDD的缓存机制
|
缓存 Java Shell
Android 系统缓存扫描与清理方法分析
Android 系统缓存从原理探索到实现。
451 15
Android 系统缓存扫描与清理方法分析
|
12月前
|
存储 缓存 监控
后端开发中的缓存机制:深度解析与最佳实践####
本文深入探讨了后端开发中不可或缺的一环——缓存机制,旨在为读者提供一份详尽的指南,涵盖缓存的基本原理、常见类型(如内存缓存、磁盘缓存、分布式缓存等)、主流技术选型(Redis、Memcached、Ehcache等),以及在实际项目中如何根据业务需求设计并实施高效的缓存策略。不同于常规摘要的概述性质,本摘要直接点明文章将围绕“深度解析”与“最佳实践”两大核心展开,既适合初学者构建基础认知框架,也为有经验的开发者提供优化建议与实战技巧。 ####
|
11月前
|
缓存 Java 数据库连接
深入探讨:Spring与MyBatis中的连接池与缓存机制
Spring 与 MyBatis 提供了强大的连接池和缓存机制,通过合理配置和使用这些机制,可以显著提升应用的性能和可扩展性。连接池通过复用数据库连接减少了连接创建和销毁的开销,而 MyBatis 的一级缓存和二级缓存则通过缓存查询结果减少了数据库访问次数。在实际应用中,结合具体的业务需求和系统架构,优化连接池和缓存的配置,是提升系统性能的重要手段。
401 4
|
11月前
|
缓存 Java 数据库连接
MyBatis缓存机制
MyBatis提供两级缓存机制:一级缓存(Local Cache)默认开启,作用范围为SqlSession,重复查询时直接从缓存读取;二级缓存(Second Level Cache)需手动开启,作用于Mapper级别,支持跨SqlSession共享数据,减少数据库访问,提升性能。
176 1
|
12月前
|
存储 缓存 监控
利用 Redis 缓存特性避免缓存穿透的策略与方法
【10月更文挑战第23天】通过以上对利用 Redis 缓存特性避免缓存穿透的详细阐述,我们对这一策略有了更深入的理解。在实际应用中,我们需要根据具体情况灵活运用这些方法,并结合其他技术手段,共同保障系统的稳定和高效运行。同时,要不断关注 Redis 缓存特性的发展和变化,及时调整策略,以应对不断出现的新挑战。
193 10
下一篇
开通oss服务