理解 Swift 中的 @inlinable (譯)

原文地址

@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穩定性的框架,否則這些屬性應該是完全安全的。不過,我強烈建議你不要使用它們,除非你知道自己在做什麼。它們是爲在大多數應用程序都無法體驗的非常特殊的情況下使用而構建的。

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