Swift 與 Objective-C

[Swift 進階 #5] Swift 分派三兄弟:final 為何能提升效能

方法呼叫會編譯為直接呼叫、vtable 或 objc_msgSend。本文整理 final 與 private 提升效能的機制、協定 extension 的陷阱,以及如何透過分析工具驗證。

閱讀 5 分鐘
[Swift 進階 #5] Swift 分派三兄弟:final 為何能提升效能 封面圖

你可能聽過「在類別加上 final 就會變快」。這聽起來像都市傳說,但背後其實有精確的機制。

本文接續上一篇文章 Swift 進階 #4

看似理所當然的方法呼叫,實際上會以三種不同方式編譯。採用哪一種,會影響效能與最佳化可能性。

進階系列本篇談的是分派。

呼叫的三張臉——直呼叫、表格與訊息

object.doSomething() 這樣的程式碼,翻譯成機器碼時有三種方式。

靜態分派。 編譯時就能確定函式,因此直接跳到函式位址呼叫。

它最快;更重要的是,編譯器能執行內嵌,移除呼叫並嵌入主體,進而開啟後續最佳化。struct 方法、全域函式與 final 方法都屬於此類。

表格分派。 支援繼承與覆寫的類別方法會採用這種方式。

即使變數的編譯期型別是 Animal,實際執行個體也可能是 Dog。要呼叫哪個實作,必須到執行期才知道。

因此每個類別都有虛擬函式表(vtable)。呼叫時會從執行個體的表格找出函式索引並跳轉。多型性的代價是一次間接參照。

協定也有稱為 witness table 的近親,作用相同。這就是 some 與 any 那一篇提到的表格。

訊息分派。 這是 Objective-C 執行環境的方式,透過 objc_msgSend 進行以名稱為基礎的查找。

三者中它最慢,但換來極高的彈性,甚至能在執行期替換方法。在 Swift 中,加上 @objc dynamic 的方法就會進入這個世界。

objc_msgSend 的內部已在另一篇文章介紹,這裡先不展開。

歸根究柢是彈性與速度的交換。越依賴執行期決定就越慢;越能在編譯期固定,就越快,也越能最佳化。

final 與 private 為何是效能關鍵字

現在可以拆解這個都市傳說了。編譯器在什麼條件下能透過去虛擬化,將表格分派靜態降級?

必須證明「這個方法不可能被覆寫」。

final 正是這項證明。final classfinal func 宣告「子類別不會重新定義」。

編譯器可以移除 vtable 查找,改成直接呼叫,也同時開啟內嵌的大門(Swift Optimization Tips)。

private 也有類似效果。檔案外不可見的宣告,讓編譯器能看見檔案內所有使用處,進而證明沒有覆寫。

啟用整個模組最佳化(WMO,現今發行組建的預設值)後,證明範圍會擴大到整個模組。沒有子類別的 internal 類別也會自動視為 final。

實務規則可以整理成一句話:沒有繼承規劃的類別就加上 final。

這不只是設計宣告(此型別是繼承樹的葉節點),對編譯器而言也是一份最佳化證明。

但別期待「加了 final,App 就變快」。呼叫開銷以奈秒計,除非位於熱迴圈中,否則幾乎無感。

真正有價值的是內嵌後連鎖發生的最佳化,而那些由編譯器自動處理。我們只需避免阻擋這項證明。

以 final 證明書拆除查找表收費站的最佳化流程圖
final 證明「沒有覆寫」,收費站就會被拆除

協定 extension 的陷阱——它是需求嗎?

分派知識在實務上最能發揮作用的地方(更準確地說,是不懂時最容易踩雷的地方)就是協定預設實作。先看一道小測驗。

protocol Greeter {
    func hello()            // 宣告為需求
}
extension Greeter {
    func hello() { print("你好") }
    func bye() { print("再見") }   // 不是需求
}
struct Korean: Greeter {
    func hello() { print("嗨") }
    func bye() { print("拜拜") }
}

let k: any Greeter = Korean()
k.hello()   // ?
k.bye()     // ?

答案是「你好」與「再見」。hello 是協定需求,因此會透過 witness table 動態分派,Korean 的實作也已註冊到表格中。

相對地,bye 是不在需求清單中的 extension 專用方法。透過協定型別呼叫時,會靜態直接呼叫 extension 實作,不論 Korean 定義了什麼。

不懂這條規則,就會變成「明明實作了,卻沒有呼叫我的程式碼」的神祕錯誤。解法很簡單。

凡是採用者必須能替換的方法,都要在協定本體宣告為需求。extension 的預設實作可以保留,但本體宣告才會在表格中建立位置。

POP(協定導向程式設計)篇曾將協定 extension 介紹為繼承的替代方案;這就是使用該工具的安全規則。

透過量測確認——不要靠感覺,要用分析工具

分派話題的結尾總該是同一個警告:這屬於微最佳化領域,而順序很重要。

先用 Instruments 的 Time Profiler 找出真正的瓶頸。大多數效能問題不是分派,而是演算法(O(n²) 迴圈)、不必要的工作(每個畫面都重新計算)或 I/O。

反對過早最佳化的提醒,就如同 Knuth 那篇文章所說。

如果分析真的抓到熱迴圈內的動態分派,這時才列出處方:讓型別成為 final、將 any 換成 some 或泛型以促成特化,或把協定邊界移到迴圈外。

反過來說,這項知識平常的用途不是最佳化,而是理解設計。

為什麼 struct 是預設值(適合靜態分派)、為什麼建議使用 some 而非 any(可進行特化),以及為什麼 SwiftUI 使用 struct view。

語言的重要決策全都扎根於這一層;了解分派後,就能把 Swift 的設計讀成一幅完整圖像。

需求方法與 extension 專用方法之分派路徑比較圖
未宣告為需求的 extension 方法會略過我的實作

總結

  • 方法呼叫會編譯為靜態(直接)、表格(vtable/witness table)或訊息(objc_msgSend)分派;彈性與速度成反比。
  • final、private 與 WMO 能證明「沒有覆寫」,將動態呼叫降為靜態呼叫並開啟內嵌。不打算繼承的類別,final 就是基本功。
  • 協定 extension 方法是否動態分派,取決於是否宣告為需求。必須可替換的方法,一定要宣告在協定本體中。
  • 順序是先用分析工具。分派知識平常的價值不是最佳化技巧,而是讀懂語言設計。

下一篇再往下一層:探討記憶體配置,包括 struct 大小如何決定、屬性順序為何會改變記憶體,以及 any 箱子的實際大小。

延伸閱讀

來源與驗證

  • Swift Optimization TipsSwift 프로젝트 · 官方文件 · 查核 2026年8月17日依據: 靜態與動態分派、final、whole-module optimization 與效能特性