Swift 開發中,為什麼要遠離 Heap?

來源:互聯網
上載者:User

標籤:數組   bar   png   必須   傳回值   細節   判斷   class   free   

Swift 開發中,為什麼要遠離 Heap?

WWDC的視頻 — Understanding Swift Performance 中,蘋果上來就說,Heap 的操作複雜度要遠遠超越 Stack。所以大家在選擇資料結構時,要盡量選擇諸如結構體這種儲存在 Stack 上的值資料類型,而不要選擇像類這種儲存在 Heap上的資料類型。問題是,相比於 Stack,Heap 操作複雜體現在什麼地方?

要回答這個問題,我們就必須瞭解 Swift 中,Heap 是用來做什麼的。同時,在 Heap 上發生了哪些操作,才導致其在效能上被詬病?

什麼是 Heap

一般提到 Heap,可能指兩種東西,一種是資料結構中的 Heap,另一種是記憶體中的 Heap。本文要談的是記憶體中的 Heap。對於資料結構的 Heap,就是一種特殊的二叉樹。它滿足以下條件:

  • 堆的最小或最大值在根節點。其所有子節點都小於或大於其父節點。
  • 堆是完全的二叉樹。除最底層所有節點都被填滿。最底層節點填充從左至右。

具體的細節這裡不作展開,網上相關的文章一大把,大家可以自行查閱。

言歸正傳。本文中,堆是可以被用來動態分配空間的記憶體塊。這個定義中有一個關鍵詞 —— 動態分配。

所謂動態分配,就是對於資料、變數,系統並不預先分配一定的空間,而是根據程式的運行和需求進行即時分配,它發生在程式調入和運行(run time)的時候。而靜態分配,是在編譯時間就已經知道資料需要的空間,所以在程式編譯和串連(compile time)時,系統就給相應資料分配了空間。舉個例子:

// assume defaultCellHeight is a static global constantstatic let defaultCellHeight = 44.0  // assume bar is a var used in a specific data structurevar bar: MyClass?

其中defaultCellHeight一看就是個浮點變數,所需的記憶體大小確定,所以它的分配是靜態分配。而bar是個類,編譯時間不知道它有多少個位元組、需要多少空間,故而只有當程式運行後,記憶體才可以確定這些細節,故而它是動態分配。

而記憶體中,負責動態分配記憶體的資料區有兩個,一個是棧(Stack),另一個是堆(Heap)。

Heap 和 Stack 在記憶體管理上的比較
  Heap Stack
結構 基於鏈表、數組、樹
特點 手動分配大小,隨時釋放空間,資料進出無序 自動分配大小,自動釋放記憶體,資料先進後出
操作 查詢之後分配/釋放,之後再做整合,複雜度高 依靠棧底指標移動來分配/釋放,複雜度低
對象 參考型別如 class。引用計數,變數類型等資訊 實值型別如 struct, enum, Int。函數傳回值,局部變數
情境 C 中的 malloc 和 free 操作,java 中的garbage collection,iOS 中的 MRC、ARC 適用於撤銷、儲存操作
線程 共用,多線程不安全 獨享,多安全執行緒
Swift 中 Heap 的設計

Swift 中 Heap 是由雙向鏈表實現的,其操作也是調用了 C++ 的 malloc 和 dealloc 方法。那麼為什麼是雙向鏈表?我們來仔細分析一下。

剛才已經說明,Stack 的操作只是指標移動,故而複雜度低,為常數。而 Heap 的操作卻十分複雜,那麼具體是怎樣的?我們不妨來看兩個底層函數:retain 和 release。

首先明確一下需求,retain 即分配空間,比如 [myString retain],就是給一個字串分配一定位元組的記憶體。而 release 即釋放之前的空間,比如[myString release],就是釋放這個字串分配的記憶體。

最直觀的設計:數組

最簡單粗暴的設計 Heap 的方法如下:將其設計成數組,其中所佔的記憶體切分成 n 等分,每一等分代表一個位元組。從左往右順序分配空間,同樣順序釋放空間,這樣所有的操作都是線性。然而想象很美好,現實卻很殘酷。假如 Heap 一共有10個位元組,我們有以下4個字串:

let string1 = "abcd" // 假設4個位元組let string2 = "a"    // 假設1個位元組let string3 = "abc"  // 假設3個位元組let string4 = "abcde"  // 假設5個位元組
然後我們做下面幾個操作:

每一步的 heap 數組長這樣,注意數字代表是記憶體大小,代表空閑:

[heap init] -> [10‘][string1 retain] -> [4, 6‘][string2 retain] -> [4, 1, 5‘][string3 retain] -> [4, 1, 3, 2‘][string3 release] -> [4, 1, 3‘, 2‘][string4 retain] -> ?

這時候我們發現,Heap 中雖然有5個位元組的空餘空間,卻無法分配給 string4,因為這5個位元組的空餘空間不連續。系統只認為有一個3位元組的空餘空間和一個2位元組的空餘空間。於是我們發現數組的想法過於天真,沒有處理 release 之後整合空餘空間的問題。

鏈表設計

於是我們想想有什麼辦法解決這個問題。假如我們利用鏈表,將所有的記憶體塊連起來,並且在 release 時通過調整鏈表指標來整合空間,這樣就能解決我們剛才的問題。順著這個思路,我們實現了下面這種 Heap 結構:

  • 記憶體塊用鏈表進行串連
  • 每個記憶體塊的頭結點表明了記憶體塊的大小,以及該記憶體塊是否已被 retain(0表示空餘,1表示已被佔用)
  • 最開頭和最末尾的節點表明已經到了 Heap 的頭和尾

Retain 操作這個時候有下面三種設計方式可控選擇:

  • 從頭遍曆鏈表。找出第一個能分配足夠空間的空閑記憶體塊。這樣的操作的複雜度是線性。
  • 從之前搜尋過的位置起搜尋鏈表的空餘記憶體塊,並找到合適的那塊。這樣可以跳過之前搜尋過的、肯定不符合的記憶體塊,一般情況下會稍微快點。
  • 從頭遍曆鏈表。找到最適合(大小最接近)的空閑記憶體塊,這樣空間利用率會很高,可惜時間複雜度上相比於之前兩種更高。

Release 的操作除了分配記憶體空間,還要注意整合記憶體塊。我們剛才那個例子,當 [string3 release]之後,它分配出來的記憶體塊會和空餘的整合在一起。這個過程如下所示:

這個設計已經及格了。但是它有一個很嚴重的問題:效能。因為一般而言,Heap 比較大,每次遍曆去找空餘空間比較耗時;其二,每次 release 之後都必須判斷當前記憶體塊的前一塊和後一塊是否為空白閑,如果是則要分別整合。這又牽涉到遍曆鏈表查詢的問題,當然解決辦法也比較簡單,用雙向鏈表。

最佳化:雙向鏈表

雙向鏈表的引入主要是引入 release 之後的記憶體塊整合問題,這樣可以快速查詢前後記憶體塊是否為空白。同時為瞭解決之前設計每次遍曆極度耗時的效能問題,我們這樣設計,我們只把空閑記憶體塊用指標連起來形成鏈表。於是 Heap 的資料結構變成了下面這樣:

這樣每次 retain 操作,我們可以少遍曆一半的記憶體(已經分配的),效率理論上來講提高一倍。而 release 操作,我們可以採用 LIFO 機制,將多出來的空餘空間插入到 Heap 頭處,並與原來的第一個空餘空間整合。這樣的做法複雜度是常數,非常高效。最後,我們再也不用花多餘的精力去用 1 和 0 來表示當前記憶體塊是否為空白。


release 操作之後整合空閑記憶體塊的一種情況

為了提高空間利用率,我們還可以引入數組。數組中的每一個元素都是一個雙向鏈表。每一個雙向鏈表串連了 Heap 中的大小相近的空餘空間。這樣我們在 retain 的時候,我們先根據大小快速定位到比較合適的空餘空間所在鏈表,再做遍曆。如此一來,空間利用率得到提高,遍曆元素數量減少,Heap 效率更高。

總結

雖然現在蘋果已經用 ARC 幫我們自動處理記憶體配置和釋放的問題了。相比於 MRC 時代手動的 retain 和 release 操作,我們無需過度擔憂記憶體調度。然而,在最佳化方面,蘋果推薦 Stack 和實值型別,是因為 Stack 的效能很高,複雜度幾乎為常數。雖然 Heap 在動態記憶體分配中似乎更自由、更靈活,但相對而言,其效能很低,複雜度較高。所以,Swift 的實值型別才如此受歡迎啊。

Swift 開發中,為什麼要遠離 Heap?

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.