iOS objc_msgSend尾呼叫優化機制詳解

QiShare發表於2019-03-02

級別:★★☆☆☆
標籤:「objc_msgSend」「尾呼叫優化」「尾遞迴」
作者: WYWMrLiuQ
審校: QiShare團隊

這篇文章的出現起因於QiShare團隊對iOS 編寫高質量Objective-C程式碼(二)中 (六、理解objc_msgSend(物件的訊息傳遞機制))的激烈討論。

這篇文章將認真徹底地分析 OC對objc_msgSend的“尾呼叫優化”。同時歡迎路過的大神留言討論。

Q1:什麼是尾呼叫?

尾呼叫(Tail Call):某個函式的最後一步僅僅只是呼叫了一個函式(可以是自身,可以是另一個函式)。

QiShare提醒:注意 “僅僅” 兩個字。

尾呼叫例子:
// 尾呼叫:
- (NSInteger)funcA:(NSInteger)num {

    /*  Some codes... */

    if (num = 0) {
        return [self funcA:num];// 尾呼叫->自身
    }    

    if (num > 0) {
        return [self funcB:num];// 尾呼叫->函式funcB
    }    

    return [self funcC:num];// 尾呼叫->函式funcC
}
複製程式碼

正例解釋:funcA的最後一步僅僅呼叫了另一個函式。不論是呼叫funcA、funcB還是funcC都屬於尾呼叫。~(不論呼叫函式的位置在哪,只要最後一步僅僅呼叫一個函式就行)~

反例:不是尾呼叫的例子
// 不是尾呼叫1:
- (NSInteger)funcA:(NSInteger)num {

    NSInteger num = [self funcB:(num)];

    return num;// 不是尾呼叫->最後一步是返回一個值,而不是呼叫一個函式
}
複製程式碼

反例解釋:不是尾呼叫。因為最後一步是返回一個值,而不是僅僅呼叫一個函式

// 不是尾呼叫2:
- (NSInteger)funcA:(NSInteger)num {

    return [self funcB:(num)] + 1;// 不是尾呼叫->原因:最後一步不僅呼叫了函式還有 +1 操作
}
複製程式碼

反例解釋:不是尾呼叫。因為最後一步不僅呼叫了函式還有 +1 操作


Q2:OC的尾呼叫優化體現在哪裡?

小編準備了一個demo:通過“斷點”和“當前記憶體情況”檢視有無尾呼叫優化

場景一:無優化 - 追加了0.0不屬於尾呼叫

無優化Demo效果圖:

無尾呼叫優化

解釋: 這種場景下,每次函式呼叫一直在進棧,不斷申請棧空間,最後會棧溢位,最終導致崩潰。
空間複雜度O(n),時間複雜度O(n)。

下面請看圖解:

iOS objc_msgSend尾呼叫優化機制詳解


場景二:有尾呼叫優化

優化Demo效果圖:

尾呼叫優化

解釋: 這種場景下,每次函式呼叫一直在重用棧幀,不申請棧空間。
空間複雜度O(1),時間複雜度O(n)。

下面請看圖解:

iOS objc_msgSend尾呼叫優化機制詳解


Q3:OC是如何實現尾呼叫優化的?

這次討論起因於《Effective Objective-C 2.0》作者的原話:

如果某函式的最後一項操作是呼叫另外一個函式,那麼就可以運用“ 尾呼叫優化 ”技術。編譯器會生成調轉至另一函式所需的指令碼,而且不會向呼叫堆疊中推入新的“棧幀”(frame stack)。只有當某函式的最後一個操作僅僅是呼叫其他函式而不會將其返回值另作他用時,才能執行“ 尾呼叫優化 ”
這項優化對objc_msgSend非常關鍵,如果不這麼做的話,那麼每次呼叫Objective-C方法之前,都需要為呼叫objc_msgSend函式準備“棧幀”,大家在“棧蹤跡”(stack trace)中可以看到這種“棧幀”。此外,如果不優化,還會過早地發生“棧溢位”(stack overflow)現象。

作者這一段概括的話,很精簡。而小編第一次看時,感覺很懵懂。在這裡,QiShare對這段話進行了詳細的分析:

  1. 尾呼叫優化的本質:很簡單,就是棧幀的複用。

  2. 尾呼叫優化的條件有三點:

    • 尾呼叫函式不需要訪問當前棧幀中的變數。(變數是形參可以,變數是實參不行)
    • 尾呼叫返回後,函式沒有語句需要執行。(最後一步僅僅只能執行一個函式)
    • 尾呼叫結果就是函式的返回值。(不能有別的“附加品”,最後一步僅僅只能是執行一個函式)
  3. 函式呼叫的過程:函式呼叫會在記憶體中申請一塊“棧幀”,儲存呼叫的地址和內部變數等資訊。如果函式A內部呼叫函式B,那麼在函式A的棧幀上就會加上一個函式B的棧幀 。如果函式B再呼叫了函式C,那麼函式A的棧幀上就會有序加上函式B和函式C的棧幀。如果C執行結束了,返回到函式B,C的棧幀才會消失。

4. 尾呼叫優化實現原理:當函式A的最後一步僅僅是呼叫另一個函式B時(或者呼叫自身函式A),這時,因為函式A的位置資訊和內部變數已經不會再用到了,直接把函式A的棧幀交給函式B使用。

  1. 尾呼叫優化關鍵圖解:
    iOS objc_msgSend尾呼叫優化機制詳解


總結:
1. 尾呼叫:某個函式的最後一步僅僅呼叫了一個函式(可以是自身,可以是另一個函式)。
2. OC的尾呼叫優化的本質是:棧幀的複用
3. 尾呼叫優化實現原理:當函式A的最後一步僅僅是呼叫另一個函式B時(或者呼叫自身函式A),這時,因為函式A的位置資訊和內部變數已經不會再用到了,直接把函式A的棧幀交給函式B使用。

PS:尾呼叫優化在Release模式下才會有,Debug模式下沒有。

本文Demo原始碼地址

關注我們的途徑有:
QiShare(簡書)
QiShare(掘金)
QiShare(知乎)
QiShare(GitHub)
QiShare(CocoaChina)
QiShare(StackOverflow)
QiShare(微信公眾號)

相關文章