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
    轉載請註明出處,謝謝。

相關文章
相關標籤/搜索