摘要
对于加了锁的缓存,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。
笔者也联想到,在服务端开发中,若数据量大到单机无法承受时,会将数据分到多个机器中,不得不说,此处做法有异曲同工之妙。