理解 Swift 中的 @inlinable (譯)

Sunxb發表於2022-06-03

@inlinable 屬性是 Swift 鮮為人知的屬性之一。與其他同類一樣,它的目的是啟用一組特定的微優化,您可以使用它們來提高應用程式的效能。讓我們來看看這個是如何工作的。

在 Swift 中使用 @inline 進行內聯擴充套件

也許最需要注意的是,雖然 @inlinable 與程式碼內聯有關,但它與我們之前已經介紹過的 @inline 屬性不同。但是為了避免您不得不閱讀兩篇文章,我們將在介紹 @inlinable 之前再次介紹這些概念。

在程式設計中,內聯擴充套件,也稱為內聯,是一種編譯器優化技術,它用所述方法的主體替換方法呼叫。

呼叫方法的操作很難做到沒有效能開銷。正如我們在關於記憶體分配的文章中所述,當應用程式希望將新的堆疊跟蹤推送到執行緒時,會進行大量的編排來傳輸、儲存和改變應用程式的狀態。雖然從方面來說,堆疊跟蹤可以簡化除錯過程,但您可能想知道是否有必要每次都這樣做。如果一個方法太簡單,呼叫它的開銷可能不僅是不必要的,而且對應用程式的整體效能有害:

func printPlusOne(_ num: Int) {
    print("My number: \(num + 1)")
}

print("I'm going to print some numbers!")
printPlusOne(5)
printPlusOne(6)
printPlusOne(7)
print("Done!")

printPlusOne 這樣的方法過於簡單,無法在應用程式的二進位制檔案中證明完整的定義。只是為了清晰起見,我們在程式碼中定義了它,但在釋出這個應用程式時最好去掉它, 用完整的實現替換所有呼叫它的地方,如下所示:

print("I'm going to print some number!")
print("My number: \(5 + 1)")
print("My number: \(6 + 1)")
print("My number: \(7 + 1)")
print("Done!")

這種減少方法呼叫開銷,可能會提高應用程式的效能,但可能會增加整體包大小,具體取決於被內聯的方法的大小(可以理解為替換,被替換的方法中的程式碼多,自然程式碼量就多了)。這個過程是由 Swift 編譯器自動完成的,根據你工程構建時的優化級別,程度是可變的。@inline 屬性可以用來忽略優化級別,強制編譯器遵循特定的方向。內聯也可以用於混淆。

@inlinable 的目的是什麼?

大多數優化,如內聯,大多在內部完成。雖然可以保證自己正在開發的模組將得到適當的優化,但當我們處理來自其他模組的呼叫時,事情會複雜得多。

編譯器優化之所以發生,是因為編譯器對正在編譯的內容有完整的瞭解,但在構建framework時,編譯器不可能知道匯入方將如何使用他。因此雖然框架的內部程式碼將得到優化,但公共介面很可能會不變。

我們可能會想,我們可以告訴編譯器組成framework的源資訊,以便它能夠重新獲得framework的資訊,並對其優化。但當我們意識到該框架的匯入器正在連結的是已經編譯的東西時,這會變得複雜。原始檔上的所有資訊都不見了,如果這是第三方框架,甚至可能一開始就沒有這些源資訊。(拿不到framework所有的源資訊)

這不是一個不可能出現的問題,雖然編譯器有很多種解決這個問題的方法,但本質是一樣的,就是讓模組的公共介面在連結時包含更多編譯器可用的資訊,這樣就可以進一步優化framework中的程式碼,不限於內聯。

在實踐中,你可能會注意到這樣做可能會嚴重失控。如果我們給公共介面的每個方法都新增資訊,這樣會讓框架的體積增大,而且大部分都會被浪費掉。我們不知道framework將會被如何使用,所以不能一概而論的這樣操作。

Swift會讓你自己決定來處理這種問題。@inlinable 屬性允許您為特定方法啟用跨模組內聯。這樣做了之後,方法的實現將作為模組公共介面的一部分來公開,允許編譯器進一步優化來自不同模組的呼叫。

@inlinable func printPlusOne(_ num: Int) {
    print("My number: \(num + 1)")
}

如果內聯方法恰好在內部呼叫其他方法,編譯器會要求您也公開這些方法。這可以通過將它們標記為 @inlinable@usableFromInline@usableFromInline 表明雖然該方法在內聯方法中使用的,但它本身並不真正應該被內聯。

@inlinable func myMethod() {
    myMethodHelper()
}

// Used within an inlinable method, but not inlinable itself
@usableFromInline internal func myMethodHelper() {
    // ...
}

使用 @inlinable 最大的收益就是有些方法可能會有效能開銷,儘管大多數的方法這種開銷可以忽略不計,但像那種包含泛型和必報的,開銷很大。

@inlinable public func allEqual<T>(_ seq: T) -> Bool where T : Sequence, T.Element : Equatable {
 // ...
}

編譯器已經應用了多種方法來解決泛型的問題。但正如我們說的,在單獨的模組內呼叫,這些方法並不適用。這種情況下, 使用@inlinable 可以帶來效能提升,不過包的大小會增加。

另一方面,當你構建時,@inlinable 可能有大問題。一個被標記為 @inlinable 的方法實現有改變時,除非重新編譯,要不然匯入他的模組無法使用他的修改。 通常你可以通過簡單地替換二進位制檔案來更新框架,但由於某些方法的實現被內聯,即使您連結到新版本,應用程式仍將繼續執行舊的版本。因此,啟用 library evolution 設定的應用程式可能會發現自己無法使用 @inlinable,因為這可能會破壞框架的ABI穩定性。

應該使用 @inlinable 還是 @inline ?

除非您正在構建一個具有ABI/API穩定性的框架,否則這些屬性應該是完全安全的。不過,我強烈建議你不要使用它們,除非你知道自己在做什麼。它們是為在大多數應用程式都無法體驗的非常特殊的情況下使用而構建的。

相關文章