[譯] 優化 Swift 的編譯時間

優化 Swift 的編譯時間

在 Swift 全部的特性中,有一件事有時會至關惱人,那就是在用 Swift 編寫更大規模的項目時,它通常會編譯多久。儘管 Swift 編譯器在保證運行時安全方面作的更多,可是它的編譯時間要比 Objective-C 編譯時間長不少。(因此)我想研究一下,是否咱們能夠幫助編譯器讓他工做的更快。javascript

因此,上週我投身於 Hyper 上的一個較大的 Swift 項目。它大概有 350 個源文件以及 30,000 行的代碼。最後我設法將這個項目的平均構建時間減小了 20%。因此我想在我這周的博客上詳細的介紹我是怎麼作的。java

如今,在咱們開始以前,我只想說我不想這篇文章以任何形式的方式來批判 Swift 或它的團隊工做。我知道 Swift 編譯器的開發者,包含 Apple 公司和開源社區,都在持續地對編譯器速度、功能和穩定性作出重大改進。但願這篇博文能隨着時間的流逝而顯得多餘,但在那以前,我只是想提供一些我發現能夠提高編譯速度的實用技巧。git

Step 1: 採集數據

在開始優化工做以前,創建一個能衡量你改進的基準老是好的。我是經過在 Xcode 裏,給應用的 target 添加兩個簡單的腳本做爲運行腳本階段來實現的。github

編譯源文件以前,添加下面的腳本:json

echo "$(date +%s)" > "buildtimes.log"複製代碼

在最後,添加這個腳本:swift

startime=$(<buildtimes.log)
endtime=$(date +%s)
deltatime=$((endtime-startime))
newline=$'\n'

echo "[Start] $startime$newline[End] $endtime$newline[Delta] $deltatime" > "buildtimes.log"複製代碼

如今,這個腳本只會測算編譯器編譯應用本身的源文件的時間(爲了測量出整個引用的編譯時間,你可使用 Xcode 的特性來掛載(hook)到 Build StartsBuild Succeeds 上)。因爲編譯時間很是依賴於編譯它的設備,因此我也 git ignored 了 buildtimes.log 文件安全

接下來,我想突出哪些個別代碼塊耗費了額外的長時間來編譯,以便識別瓶頸,這樣我就能夠修復它。要作到這個,只須要經過向 Xcode 中 Build Setting 裏的 Other Swift Flags 傳遞下面的參數給 Swift 編譯器來設置一個臨界值:frontend

-Xfrontend -warn-long-function-bodies=500複製代碼

使用上面的參數後,在你的項目中,若是有任何函數耗費了超過 500 毫秒的編譯時間,你就會獲得一個警告。這是我開始設置的臨界值(而且隨着我對更多瓶頸的修復,這個值在不斷的下降)。函數

Step 2: 消除全部的警告

在設置了函數編譯時間過長的警告以後,你可能會在項目中開始發現一些。最開始,你會以爲編譯時間過長的函數是隨機的,可是很快模式(patterns)就開始出現了。這裏我注意到了兩個使 Swift 3.0 編譯器編譯函數時間過長的常見模式:佈局

自定義運算符(特別是帶有通用參數的重載)

當 Swift 出現時,對於大多數 iOS 和 macOS 開發者來講,運算符重載是全新的概念之一,但就像許多新鮮事物同樣,咱們很興奮的使用它們。如今,我不打算在這討論自定義或重載運算符是好是壞,但它們的確對編譯時間有很大影響,尤爲是若是使用更加複雜的表達式。

思考下面的運算符,它將兩個 IntegerConvertible 類型的數字加起來,構成了自定義的數字類型:

func +<A: IntegerConvertible, B: IntegerConvertible>(lhs: A, rhs: B) -> CustomNumber { return CustomNumber(int: lhs.int + rhs.int) }複製代碼

而後咱們用它來讓幾個數字相加:

func addNumbers() -> CustomNumber {
    return CustomNumber(int: 1) +
           CustomNumber(int: 2) +
           CustomNumber(int: 3) +
           CustomNumber(int: 4) +
           CustomNumber(int: 5)
}複製代碼

看上去很簡單,可是上面的 addNumbers() 函數會花費很長一段時間來編譯(在我 2013 年的 MBP 上超過 300 ms)。對比一下,若是咱們用協議擴展來實現相同邏輯:

extension IntegerConvertible {
    func add<T: IntegerConvertible>(_ number: T) -> CustomNumber {
        return CustomNumber(int: int + number.int)
    }
}

func addNumbers() -> CustomNumber {
    return CustomNumber(int: 1).add(CustomNumber(int: 2))
                               .add(CustomNumber(int: 3))
                               .add(CustomNumber(int: 4))
                               .add(CustomNumber(int: 5))
}複製代碼

經過這個改變,咱們的 addNumbers() 函數如今編譯時間不到 1 ms這快了 300 倍!

因此,若是你大量的使用了自定義/重載運算符,特別是帶有通用參數的(或者若是你使用的第三方庫來作這些,好比許多自動佈局的庫),考慮一下用普通函數、協議擴展或其餘的技術來重寫吧。

集合字面量

另外一個我發現的編譯時間瓶頸是使用集合字面量,特別是編譯器須要作不少工做來推斷那些字面量的類型。讓咱們假設你有一個函數,它要把模型轉換成一個相似 JSON 的字典,像這樣:

extension User {
    func toJSON() -> [String : Any] 
        return [
            "firstName": firstName,
            "lastName": lastName,
            "age": age,
            "friends": friends.map { $0.toJSON() },
            "coworkers": coworkers.map { $0.toJSON() },
            "favorites": favorites.map { $0.toJSON() },
            "messages": messages.map { $0.toJSON() },
            "notes": notes.map { $0.toJSON() },
            "tasks": tasks.map { $0.toJSON() },
            "imageURLs": imageURLs.map { $0.absoluteString },
            "groups": groups.map { $0.toJSON() }
        ]
    }
}複製代碼

上面 toJSON() 函數在個人電腦上大概要 500 ms 的時間來編譯。如今讓咱們試着逐行重構這個像字典的東西來代替字面量:

extension User {
    func toJSON() -> [String : Any] {
        var json = [String : Any]()
        json["firstName"] = firstName
        json["lastName"] = lastName
        json["age"] = age
        json["friends"] = friends.map { $0.toJSON() }
        json["coworkers"] = coworkers.map { $0.toJSON() }
        json["favorites"] = favorites.map { $0.toJSON() }
        json["messages"] = messages.map { $0.toJSON() }
        json["notes"] = notes.map { $0.toJSON() }
        json["tasks"] = tasks.map { $0.toJSON() }
        json["imageURLs"] = imageURLs.map { $0.absoluteString }
        json["groups"] = groups.map { $0.toJSON() }
        return json
    }
}複製代碼

它如今編譯時間大概在 5 ms 左右,提升了 100 倍!

Step 3: 結論

上面的兩個例子很是清晰的說明了 Swift 編譯器的一些新特性,好比類型推演和重載,都是付出了時間開銷。若是咱們仔細思考一下,也很符合邏輯。因爲編譯器不得不作更多的工做來執行推演,因此花費了更多的時間。可是咱們也看到了,若是咱們稍微調整一下咱們的代碼,幫助編譯器更簡單的解決表達式,咱們就能夠很大程度的加快編譯時間。

如今,我不是說你要一直讓編譯時間來決定你寫代碼的方式。有時可讓它作更多的工做,讓你的代碼更加清晰而且容易理解。可是在大型的項目中,每一個函數要用 300-500 ms 範圍(或更多)的時間來編譯的編碼技術可能很快就會成爲一個問題。個人建議是對你的編譯時間保持監控,使用上面的編譯標記設置一個合理的臨界值,並在發現問題的時候解決問題。

我確信上面的例子確定沒有涵蓋全部潛在的編譯時間改進的方法,全部我很願意聽到你的意見。若是你有任何有用的改進大型 Swift 項目編譯時間的其餘的技術,你能夠寫在 Medium 上回復,或者在 Twitter @johnsundell 上聯繫我。

感謝閱讀!🚀

相關文章
相關標籤/搜索