Objc StripedMap 优化加锁缓存

542 阅读3分钟

摘要

对于加了锁的缓存,StripedMap 可以起到提高访问速度的作用。

典型应用场景: SideTable。

StripedMap 是什么?

从注释中可以了解到,它可用来优化加了锁的缓存。

// StripedMap<T> is a map of void* -> T, sized appropriately
// for cache-friendly lock striping.
// For example, this may be used as StripedMap<spinlock_t>
// or as StripedMap<SomeStruct> where SomeStruct stores a spin lock.
template<typename T>
class StripedMap {
#if TARGET_OS_IPHONE && !TARGET_OS_SIMULATOR
    enum {StripeCount = 8}; // 对于 iPhone 而言,可同时存在 8 个 SideTable
#else
    enum {StripeCount = 64};
#endif

    struct PaddedT {
        T value alignas(CacheLineSize);
    };

    PaddedT array[StripeCount];
    // 根据地址,返回的 index,用来从 array 中取值
    static unsigned int indexForPointer(const void *p) {
        uintptr_t addr = reinterpret_cast<uintptr_t>(p);
        // 最核心的一行代码
        return ((addr>> 4) ^ (addr >> 9)) % StripeCount;
    }
    // 后面还有好几个 lock 方法
    ...
}

笔者觉得可将其看成是缓存的「管理者」。

主要用来解决这一问题:

加了锁的缓存,访问速度会相应地变慢,尤其在并发量大的情况下。

而 StripedMap 给出的解决方案:

直接多几个缓存,使用时再分配合适的即可。

应用场景

典型场景 SideTable。

比如说在保存 weak 关系的函数里,就有它的身影。

static StripedMap<SideTable>& SideTables() {
    return *reinterpret_cast<StripedMap<SideTable>*>(SideTableBuf);
}

storeWeak(id *location, objc_object *newObj)
{
    ...
 retry:
    if (haveOld) {
        oldObj = *location;
        oldTable = &SideTables()[oldObj]; // 使用了 StripedMap
    } else {
        oldTable = nil;
    }
    if (haveNew) {
        newTable = &SideTables()[newObj];
    } else {
        newTable = nil;
    }
    ...
}

如何做到的?

其实核心逻辑就是一行代码:((addr>> 4) ^ (addr >> 9)) % StripeCount;

笔者在搜索相关资料时,看到这个 回答 中的一句:

The system can maintain 8 weak_table_t cocurrently at most for 8 thread.

误以为 StripedMap 可保证每个线程使用一个缓存。

为此写了如下代码测试:

((addr>> 4) ^ (addr >> 9)) % StripeCount 能否保证同一线程,会计算出来同个值?

dispatch_async(dispatch_get_global_queue(0, 0), ^{
    {
        NSObject *obj = [NSObject new];
        uintptr_t addr = &obj;
        int r = ((addr>> 4) ^ (addr >> 9)) % 8;
        NSLog(@"%@, %p -> %d", [NSThread currentThread], obj, r);
    }

    {
        NSObject *obj = [NSObject new];
        uintptr_t addr = &obj;
        int r = ((addr>> 4) ^ (addr >> 9)) % 8;
        NSLog(@"%@, %p -> %d", [NSThread currentThread], obj, r);
    }
});

打印结果如下,可以看出来,虽然是同一个线程,计算出来的 index 却是不同的。

<NSThread: 0x600000024c40>{number = 6, name = (null)}, 0x600001768180 -> 7
<NSThread: 0x60000002b800>{number = 5, name = (null)}, 0x600001748000 -> 0

所以,笔者认为它只是做了分流,让并发的线程「尽量」不会一窝蜂挤向同一个缓存。

至于为什么简单的一行 ((addr>> 4) ^ (addr >> 9)) % StripeCount 就可以做到这点?

是不是跟内存地址规律有关系呢?比如说创建时间比较接近的对象地址会有某种规律?

笔者实在没能搞懂,望有知道的读者可以告知一声,不胜感激!

小结

StripedMap 可「加速访问」加了锁的缓存。

其内部会根据对象地址,巧妙地让并发的线程「尽可能地」访问不同的缓存。

而典型应用场景是:存储 refCount「引用计数」、weakTable「弱引用关系」的 SideTable。

笔者也联想到,在服务端开发中,若数据量大到单机无法承受时,会将数据分到多个机器中,不得不说,此处做法有异曲同工之妙。