iOS管理对象内存的数据结构以及操作算法--SideTablesRefcountMapweak_table_t

Posted Jsen_Wang

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了iOS管理对象内存的数据结构以及操作算法--SideTablesRefcountMapweak_table_t相关的知识,希望对你有一定的参考价值。

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

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

一、分离锁

    分离锁并不是一种锁,而是一种对锁的用法。(下面继续上一张感人的手绘图。哈哈我写的字也出现了。如果比写字丑,一般很少有人能比我写的丑)

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

 

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

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

  • 图1因为任何操作都需要锁整张表,所以写操作的时候相当于串行操作。没有并发。
  • 图2因为每一个单元格都有一把锁,所以写操作的时候有多少个单元格并发数就可以是多少。

这里注意区分一下并发和并行的区别

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

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

二、为什么SideTables会冲突、SideTable又扮演着什么角色、RefcountMap是用来干嘛的?

    下面我用一个不太恰当的例子来说明问题
假设有80个学生需要咱们安排住宿,同时还要保证学生们的财产安全。应该怎么安排?
    显然不会给80个学生分别安排80间宿舍,然后给每个宿舍的大门上加一把锁。那样太浪费资源了锁也挺贵的,太多的宿舍维护起来也很费力气。
    我们一般的做法是把80个学生分配到10间宿舍里,每个宿舍住8个人。假设宿舍号分别是101、102 、... 110。然后再给他们分配床位,01号床、02号床等。然后给每个宿舍配一把锁来保护宿舍内同学的财产安全。为什么不只给整个宿舍楼上一把锁,每次有人进出的时候都把整个宿舍楼锁上?显然这样会造成宿舍楼大门口阻塞。
    OK假如现在有人要找102号宿舍的2号床的人聊天。这个人会怎么做?

  • 1、找到宿舍楼(SideTables)的宿管,跟他说自己要找10202(内存地址当做key)。
  • 2、宿管带着他SideTables[10202]找到了102宿舍SideTable,然后把102的门一锁lock,在他访问102期间不再允许其他访客访问102了。(这样只是阻塞了102的8个兄弟的访问,而不会影响整栋宿舍楼的访问)
  • 3、然后在宿舍里大喊一声:"2号床的兄弟在哪里?"table.refcnts.find(02)你就可以找到2号床的兄弟了。
  • 4、等这个访客离开的时候会把房门的锁打开unlock,这样其他需要访问102的人就可以继续进来访问了。

 

SideTables == 宿舍楼
SideTable  == 宿舍
RefcountMap里存放着具体的床位

    苹果之所以需要创造SideTables的Hash冲突是为了把对象放到宿舍里管理,把锁的粒度缩小到一个宿舍SideTable。RefcountMap的工作是在找到宿舍以后帮助大家找到正确的床位的兄弟。

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

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

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

  • 1、我们的key值分部是否平均。

  • 2、我们采用的散列算法能不能尽量把输出值平均分配。

    1、在数据量足够大的情况下我们的key值分部是平均的。因为key值是内存地址。从低位0x0000...0000到高位0xffff...ffff分配。并且操作系统的内存管理模块本身也会对内存分配做很多优化。毕业年头长了,内管管理具体的细节我也扯不出来了。赶紧贴一片文章压压惊,有兴趣的同学可以看操作系统内存管理——分区、页式、段式管理
    2、那么SideTables使用的Hash算法是什么呢?我们来开一个新的大标题。

四、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;

  • 1、将对象的内存地址addr右移4位得到结果1

  • 2、将对象的内存地址addr右移9位得到结果2

  • 3、将结果1和结果2做按位异或得到结果3

  • 4、将结果3和StripeCount做模运算得到真正的Hash值。

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

五、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())表示中途找到了。



作者:ios入门级攻城尸
链接:https://www.jianshu.com/p/8577286af88e

以上是关于iOS管理对象内存的数据结构以及操作算法--SideTablesRefcountMapweak_table_t的主要内容,如果未能解决你的问题,请参考以下文章

iOS管理对象内存的数据结构以及操作算法--SideTablesRefcountMapweak_table_t

iOS管理对象内存的数据结构以及操作算法--SideTablesRefcountMapweak_table_t

iOS的内存管理和引用计数规则Block的用法以及三种形式(stackmallocglobal)

iOS的内存管理和引用计数规则Block的用法以及三种形式(stackmallocglobal)

iOS开发-33学习手动内存管理临时抛弃ARC以及retain/assign知识——iOSproject师面试必考内容

java常用的数组字符串集合操作以及数据结构与算法基本知识