Objective-C runtime源码小记-StripedMap

1,458 阅读7分钟

本文基于可编译的objc4-750版本源码进行讨论,文章中旦有错误之处,希望大家不吝指正。

当我们通过阅读源码来研究weak实现原理,层层下潜来到 weak_entry_t 之前,绕不开 StripedMap<SideTable> 这样的结构;或者仅仅想了解property的各个关键字在实现上的区别而来到 reallySetProperty() 方法中,也免不了看到 StripedMap<spinlock_t> 这样的东西,假设我们停下来点进去探究一番,会来到一个叫做StripedMap<T>的模板类,它的名字中带"Map",粗粗一撇也是类似hashMap的实现,不管怎样,它与我们正在研究的问题看起来无关紧要,我们把它简单看做一个hashMap也无伤大雅,很多博客中也是三两句简单带过。但是当我某次在其中多做了一些停留,亦令我小有收获。本篇文章正是记录我探究StripedMap<T>的小小心得。

先说结论:
  1. 它的主要作用是缓存"带spinlock锁能力的类或者结构体"(比如:spinlock_tSideTable),它内部存放的结构个数是固定且有限的(由其内部array的容量决定,iPhone真机通常是8个)。
  2. 它使用对象的地址作为key,取得对应的<T>
  3. 它对<T>的访问起到了分流的作用,而对<T>的访问进行分流是为了避免在高频调用的地方,因为访问<T>而产生的性能瓶颈,优化了整体的效率。
再看使用场景:

我们来看两个StripedMap<T>使用的场景:

  1. StripedMap<spinlock_t>
// 1.申明
StripedMap<spinlock_t> PropertyLocks;

// 2.设置属性的实现
static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
    // ...
    
    spinlock_t& slotlock = PropertyLocks[slot];
    slotlock.lock();
    // ...    
    slotlock.unlock();
    
    // ...
}

调用默认的属性设置方法时,最终会来到这个函数中。其中,实际给对象指针赋值的操作,使用了spinlock_t锁进行保护,这边的spinlock_t锁是从预先申明和初始化好的PropertyLocks(StripedMap<spinlock_t>结构)中取得。

  1. StripedMap<SideTable>
// 1. SideTable结构体
struct SideTable 结构 {
    spinlock_t slock;
    // ...
    weak_table_t weak_table;

    // ...
    template<HaveOld, HaveNew>
    static void lockTwo(SideTable *lock1, SideTable *lock2);
    template<HaveOld, HaveNew>
    static void unlockTwo(SideTable *lock1, SideTable *lock2);
};

// 2. SideTables是一个预先初始化好的StripedMap<SideTable>
static StripedMap<SideTable>& SideTables() {
    return *reinterpret_cast<StripedMap<SideTable>*>(SideTableBuf);
}

// 3. 设置weak对象的内部实现
static id 
storeWeak(id *location, objc_object *newObj)
{
    // ...
    oldTable = &SideTables()[oldObj];
    // ...
    newTable = &SideTables()[newObj];
    
    // ...
    SideTable::lockTwo<haveOld, haveNew>(oldTable, newTable); // 同时对两个资源加锁,保证两个锁的顺序
    // ... (设置)
    SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable); // 同时对两个资源解锁,保证两个锁的顺序
    // ...
}

隐藏了一些跟这边讨论主题无关的代码,SideTable结构包含了一个spinlock_t,以及一个weak_table_t。可以这么理解,每一个weak所引用的对象,都对应一个SideTable,引用该对象的所有weak指针,都存放在SideTable的weak_table中。所以在对weak对象进行设置的时候,实际来到storeWeak()方法,对weak_table操作的时候,对应的SideTable也会用spintlock进行保护。

从以上两个场景看来,都是为保证操作的原子性而使用spinlock加锁。设想:第1个场景没有用StripedMap<spinlock_t>的对象,仅在全局初始化一个spinlock_t,所有需要lock和unlock的地方都用这个spinlock_t,是完全可以的;第二个场景也一样,如果不用StripedMap,而全局只初始化一个SideTable对象,也未尝不可。

但是,在如此频繁调用的地方,如果只有一个spinlock_t或SideTable的话,一次只能操作一个对象,对其它对象的操作只能干等,必然造成性能瓶颈。然而为每次调用都创建一个spinlock_t或SideTable,相当于为每个对象都创建一个也是不现实也没有必要的。为此,准备多个spinlock_t或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 {
    // ... 
    // 如果是iPhone 且不是模拟器,初始化数组容量为8,其他设备初始化数组容量为64
    enum { StripeCount = 8 };
    // ...

    struct PaddedT {
        // alignas是c++的对齐修饰符, 指定对齐值
        // 包含一个T类型的的成员变量value,并且value为 CacheLineSize 字节对齐,
        // Cache Line可以简单的理解为CPU Cache中的最小缓存单位。目前主流的CPU Cache的Cache Line大小都是64Bytes。
        // 和cacheline大小对齐,是一种优化效率的有效方法
        // 具体可以参考这篇文章了解: http://cenalulu.github.io/linux/all-about-cpu-cache/
        T value alignas(CacheLineSize);
    };

    PaddedT array[StripeCount];

    // (p指针右移4位) 异或 (p指针右移9位) 之后进行取余操作 获得一个数组的index
    // 异或:二进制位相同得0,不同得1
    static unsigned int indexForPointer(const void *p) {
        uintptr_t addr = reinterpret_cast<uintptr_t>(p);
        return ((addr >> 4) ^ (addr >> 9)) % StripeCount;
    }

 public:
    T& operator[] (const void *p) { 
        return array[indexForPointer(p)].value; 
    }
    const T& operator[] (const void *p) const { 
        return const_cast<StripedMap<T>>(this)[p]; 
    }
    
    // Shortcuts for StripedMaps of locks.
    // ...
    
    constexpr StripedMap() {}
};

("//..."处为省略的代码)

该结构是一个以void *作为key,value的类型为<T>的一个map,看起来像是一个Dictionary(HashMap)。从注释中我们可以看出,其中<T>主要是spinlock_t,或者包含spinlock_t的其他类型(比如SideTable)。从其提供的public方法中也可以看到,很多是锁操作相关的方法,从中也可以得知,其主要作用是用于缓存spinlock。

从以下这行看到,StripedMap中有一个实际存放value(为<T>类型)的固定大小的数组:

PaddedT array[StripeCount];

这意味着,value的总数固定,并且value在StripedMap创建的时候就填充好了,假设StripedMap创建时为StripedMap<spinlock_t>,那么其已创建完后就存放着8个spinlock_t。如果StripedMap创建时为StripedMap<SideTable>,那么其已创建完后就存放着8个SideTable。

那么如何根据void *指针获取对应的value值呢,通过这个方法:

// (p指针右移4位) 异或 (p指针右移9位) 之后进行取余操作 获得一个数组的index
// 异或:二进制位相同得0,不同得1
static unsigned int indexForPointer(const void *p) {
    uintptr_t addr = reinterpret_cast<uintptr_t>(p);
    return ((addr >> 4) ^ (addr >> 9)) % StripeCount;
}

该方法作用很简单,就是根据指针地址获得一个数组的index值,而且位操作的方式确保其非常高效。该方法类似于Dictionary中的hash方法,只是,hash方法会确保每个key求得的值尽可能都是唯一的,即使少数情况不唯一的话,内部也有冲突处理机制,确保对应的value是唯一的。而该方法所有的key求得的值必然落在有限的数组index中,所以是会重复的,如果传入足够多的key,那么它应该会尽可能均匀的访问到数组中的各个value。

画一个不太严谨的图表示: alt

最后:
  • StripedMap是一个特殊的Map,也确实跟HashMap有某些相似之处,比如:采用key-value的方式进行存储,可以通过key获得value的值。但实际上不是一个HashMap,因为HashMap的一个key只能对应唯一的value,而StripedMap的一个value可以通过多个key访问。
  • 它真正的作用是为了对有限的资源的访问做缓存和分流!(整个objc源码中共搜索到三种:分别是spinlock_tSideTableSyncList)
  • 事实上,早期版本的objc源码没有StripedMap,设想一下,对于需要频繁调用的地方,如果只有一个spinlock,所有的访问都需要等待那一个锁,必然造成性能瓶颈。现在使用StripedMap进行分流,性能自然大大提高。
  • 不同设备的数组容量(StripeCount)根据宏定义会有所不同,比如iPhone真机的数组锁容量为8,其他设备(包括模拟器)的数组容量为64。