iOS管理对象内存的数据结构以及操做算法--SideTables、RefcountMap、weak_table_t-二

    这篇文章是以前那篇文章iOS管理对象内存的数据结构以及操做算法--SideTables、RefcountMap、weak_table_t的补充和延伸。若是没有阅读过前一篇文章请先看那一篇。
    上一篇文章中关于SideTables、SideTable和RefcountMap三者关系可能描述的不太清楚。不少朋友表示看起来晕乎乎的。当初我在研究的时候也是蒙圈了好长一段时间。因此特地写了这篇文章来补充说明一下,同时也有新的知识扩展。
    刚开始写文章,思路不太清晰。若是文章中有什么错误或者问题,欢迎你们指正。
    这里特别感谢大墙66370午夜时分挑灯夜战提出的宝贵问题。javascript

本文流程
1、解释分离锁是什么。
2、举例阐述SideTables、SideTable、RefcountMap三者关系。
3、第一篇文章所说的N路并发是什么意思。
4、SideTables所使用的Hash算法解密。
5、RefcountMap是什么结构。html

1、分离锁

    分离锁并非一种锁,而是一种对锁的用法。(下面继续上一张感人的手绘图。哈哈我写的字也出现了。若是比写字丑,通常不多有人能比我写的丑)
java

6DA5F9AD-73C5-4888-9E03-1C72D0E848F1.png

    图1这样对一整个表加一把锁,是咱们平时比较常见的。若是我一次写操做须要操做表中多个单元格的数据,好比第一次操做0、一、2位置的数据,第二次操做0、二、3位置的数据。像这种状况锁的粒度就是以整张表为单位的,才能保证数据的安全。
    图2这样对表中的各个元素分别加一把锁就是咱们说的分离锁。适用于表中元素相互独立,你对第一个元素作写操做的时候不须要影响到其余元素。
    上文中所说的结构就是SideTables这个大的Hash表中每个小单元格(SideTable)都带有一把锁。作写操做的时候(操做对象引用计数)单元格之间相互独立,互相没影响。因此下降了锁的粒度。算法

对比一下图1和图2的并发性。安全

  • 图1由于任何操做都须要锁整张表,因此写操做的时候至关于串行操做。没有并发。
  • 图2由于每个单元格都有一把锁,因此写操做的时候有多少个单元格并发数就能够是多少。

这里注意区分一下并发和并行的区别数据结构

当有多个线程在操做时,若是系统只有一个CPU,则它根本不可能真正同时进行一个以上的线程,它只能把CPU运行时间划分红若干个时间段,再将时间段分配给各个线程执行,在一个时间段的线程代码运行时,其它线程处于挂起状态.这种方式咱们称之为并发(Concurrent).
当系统有一个以上CPU时,则线程的操做有可能非并发.当一个CPU执行一个线程时,另外一个CPU能够执行另外一个线程,两个线程互不抢占CPU资源,能够同时进行,这种方式咱们称之为并行(Parallel)并发

    能够看到在单元格之间相互独立的状况下图2的方法效率更高。
    看了上面的例子有同窗会有疑问。既然分离锁能够实现高并发,那么为何不对每个内存对象加一把锁呢?为何还会有Hash表还会冲突呢?这个问题我在下面经过一个例子和RefcountMap一块儿解释。ide

##2、为何SideTables会冲突、SideTable又扮演着什么角色、RefcountMap是用来干吗的?
    下面我用一个不太恰当的例子来讲明问题
    假设有80个学生须要我们安排住宿,同时还要保证学生们的财产安全。应该怎么安排?
    显然不会给80个学生分别安排80间宿舍,而后给每一个宿舍的大门上加一把锁。那样太浪费资源了锁也挺贵的,太多的宿舍维护起来也很费力气。
    咱们通常的作法是把80个学生分配到10间宿舍里,每一个宿舍住8我的。假设宿舍号分别是10一、102 、... 110。而后再给他们分配床位,01号床、02号床等。而后给每一个宿舍配一把锁来保护宿舍内同窗的财产安全。为何不仅给整个宿舍楼上一把锁,每次有人进出的时候都把整个宿舍楼锁上?显然这样会形成宿舍楼大门口阻塞。
    OK假如如今有人要找102号宿舍的2号床的人聊天。这我的会怎么作?高并发

  • 一、找到宿舍楼(SideTables)的宿管,跟他说本身要找10202(内存地址当作key)。
  • 二、宿管带着他SideTables[10202]找到了102宿舍SideTable,而后把102的门一锁lock,在他访问102期间再也不容许其余访客访问102了。(这样只是阻塞了102的8个兄弟的访问,而不会影响整栋宿舍楼的访问)
  • 三、而后在宿舍里大喊一声:"2号床的兄弟在哪里?"table.refcnts.find(02)你就能够找到2号床的兄弟了。
  • 四、等这个访客离开的时候会把房门的锁打开unlock,这样其余须要访问102的人就能够继续进来访问了。
    SideTables == 宿舍楼
    SideTable  == 宿舍
    RefcountMap里存放着具体的床位复制代码

    苹果之因此须要创造SideTables的Hash冲突是为了把对象放到宿舍里管理,把锁的粒度缩小到一个宿舍SideTable。RefcountMap的工做是在找到宿舍之后帮助你们找到正确的床位的兄弟。优化

3、N路的并发写操做那句话是什么意思?

上一篇文章中提到:
由于是使用对象的内存地址当key因此Hash的分部也很平均。假设Hash表有n个元素,则能够将Hash的冲突减小到n分之一,支持n路的并发写操做。

    咱们在分配宿舍的时候是给同窗分配宿舍和床位,而后再给宿舍和床位编号。因此咱们能够很平均的给每一个宿舍分配8我的。
    那么若是咱们用对象内存地址当作Hash算法的key,所获得的散列值可能会出现某些宿舍分配了4我的,某些宿舍分配了12我的的状况。这样人员分配就不平均了。若是某一时间段正好这个宿舍的12我的的访问量都特别大,那么访问起来就又会出现阻塞了。而那4人间的宿舍就会闲置,形成了资源的浪费。会不会形成这种资源浪费主要看两点。

  • 一、咱们的key值分部是否平均。
  • 二、咱们采用的散列算法能不能尽可能把输出值平均分配。

    一、在数据量足够大的状况下咱们的key值分部是平均的。由于key值是内存地址。从低位0x0000...0000到高位0xffff...ffff分配。而且操做系统的内存管理模块自己也会对内存分配作不少优化。毕业年头长了,内管管理具体的细节我也扯不出来了。赶忙贴一片文章压压惊,有兴趣的同窗能够看操做系统内存管理——分区、页式、段式管理
    二、那么SideTables使用的Hash算法是什么呢?咱们来开一个新的大标题。

4、SideTables所使用的Hash算法解密。

    SideTables的定义:NSObject.mm line 207-209

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

    若是看不懂不要紧,当它是一个C++的Map。我们来看StripedMap的定义:objc-private.h line 867-906
其中有用的部分是这样的

...
//若是是嵌入式系统StripeCount=8。咱们这里StripeCount=64
enum { StripeCount = 64 };
...
static unsigned int indexForPointer(const void *p) {
    //这里是类型转换,不用在乎
    uintptr_t addr = reinterpret_cast<uintptr_t>(p);

    //这里就是咱们要找的Hash算法了
    return ((addr >> 4) ^ (addr >> 9)) % StripeCount;
}复制代码
  • 一、将对象的内存地址addr右移4位获得结果1
  • 二、将对象的内存地址addr右移9位获得结果2
  • 三、将结果1和结果2作按位异或获得结果3
  • 四、将结果3和StripeCount作模运算获得真正的Hash值。

    由于最后模运算的结果范围是在0-63之间,可见SideTables一共有64个单元格。

5、RefcountMap是什么结构。

    前面文章中提到过if(引用计数器 != table.refcnts.end())所以有同窗提问end()是什么?那么我们得研究一下RefcountMap是什么类型的
    看定义NSObject.mm line 137

typedef objc::DenseMap<DisguisedPtr<objc_object>,size_t,true> RefcountMap;复制代码

    看起来好复杂,先无论它。一路顺着定义找下去。找到DenseMap ==> DenseMapBase ==> inline iterator end()发现我们当前在llvm-DenseMap.h line 77-79。艾玛吓死我了怎么看到llvm了。llvm我也不懂,因此仍是不要扯的太远。赶忙打开llvm的相关文档看看DenseMapBase中的公开方法有

09719F81-A6B1-4B4E-A906-DAD8F260B052.png

    固然还有更多其余方法,我只是截取了一部分。经过这部分咱们能够看出来咱们操做的refcnts.find()和refcnts.end()其实都是对一个C++迭代器iterator的操做。而end()的状态表示的是从头开始查找,一直找到最后都没有找到。当前指针指向的是结束位,而不是最后一个元素。
    因此 if(引用计数器 == table.refcnts.end())表示查找到最后都没找到if(引用计数器 != table.refcnts.end())表示中途找到了。

    讲到这里想讲的内容已经讲完啦,欢迎评论、点赞、转发。
    欢迎加个人微博weibo.com/xuyang186
    转载请注明出处,谢谢。

相关文章
相关标签/搜索