iOS管理對象內存的數據結構以及操作算法--SideTables、RefcountMap、weak_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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章