[翻譯]關於Swift的編譯時間優化

VernonVan發表於2017-10-24
原文連結:Regarding Swift build time optimizations

上週,在我讀完 @nickoneill 寫的一篇優秀的博文《為緩慢的Swift編譯時間提速》後,我發現用一個不同的角度去審視 Swift 程式碼並不是很難的一件事。

可以被認為是簡潔的一行程式碼現在引發了一個新的問題 -- 是否應該把這行程式碼重構成對應的9行程式碼以讓編譯器更容易工作(看看接下來要講的關於空合運算子(nil coalescing operator)的示例)?到底哪個才是更重要的,簡潔的程式碼還是對編譯器友好的程式碼?這取決於專案的大小和開發者的想法。

慢著。。。這裡有一個 Xcode 外掛

在展示具體的例子之前,我先想到就是手動檢視日誌是一件非常耗時的事情。有人提出了用終端命令可以讓這件事情變得比較容易,但是我更進一步,把這個用 Xcode 外掛 給實現出來了。

[翻譯]關於Swift的編譯時間優化

對我來說,最初的目的就是找到並修復最耗時的地方,但我現在的想法是讓它經歷更多的迭代過程。這樣的話我就不僅可以讓程式碼編譯更有效率,還可以防止第一次進入專案的耗時。

更加驚喜的是

我經常在多個 Git 分支間跳來跳去,等待一個緩慢的專案編譯完成往往浪費了大量的時間。我想了好長一段時間為什麼我的一個寵物專案會編譯地這麼緩慢(大概2萬行 Swift 程式碼)。

在我學習了究竟是原因導致的這件事之後,我不得不承認我的確很吃驚,一行程式碼就需要幾秒鐘來編譯。

讓我們一起看看幾個例子。

空合操作符

編譯器是很不喜歡這裡的第一種方式的。在展開下面兩處簡寫的程式碼之後,編譯時間減少了99.4%。

// 編譯時間: 5238.3ms
return CGSize(width: size.width + (rightView?.bounds.width ?? 0) + (leftView?.bounds.width ?? 0) + 22, height: bounds.height)

// 編譯時間: 32.4ms
var padding: CGFloat = 22
if let rightView = rightView {
    padding += rightView.bounds.width
}

if let leftView = leftView {
    padding += leftView.bounds.width
}
return CGSizeMake(size.width + padding, bounds.height)
複製程式碼


ArrayOfStuff + [Stuff]

這個看起來像下面這樣:

return ArrayOfStuff + [Stuff]
// 而不是
ArrayOfStuff.append(stuff)
return ArrayOfStuff複製程式碼
我經常這樣做,每次都會對所需的編譯時間產生影響。下面是最差的一個,這裡的編譯時間減少了97.9%。

// 編譯時間: 1250.3ms
let systemOptions = [ 7, 14, 30, -1 ]
let systemNames = (0...2).map{ String(format: localizedFormat, systemOptions[$0]) } + [NSLocalizedString("everything", comment: "")]
// 一些中間的程式碼
labelNames = Array(systemNames[0..<count]) + [systemNames.last!]

// 編譯時間: 25.5ms
let systemOptions = [ 7, 14, 30, -1 ]
var systemNames = systemOptions.dropLast().map{ String(format: localizedFormat, $0) }
systemNames.append(NSLocalizedString("everything", comment: ""))
// 一些中間的程式碼
labelNames = Array(systemNames[0..<count])
labelNames.append(systemNames.last!)
複製程式碼


三元運算子

僅僅只是把三元運算子替換成 if-else 語句,就讓編譯時間減少了92.9%。如果將 map 換成 for 迴圈,就又能減少75%(但是那樣的話我的眼睛可就受不了了)。?

// 編譯時間: 239.0ms
let labelNames = type == 0 ? (1...5).map{type0ToString($0)} : (0...2).map{type1ToString($0)}

// 編譯時間: 16.9ms
var labelNames: [String]
if type == 0 {
    labelNames = (1...5).map{type0ToString($0)}
} else {
    labelNames = (0...2).map{type1ToString($0)}
}複製程式碼

轉換 CGFloat 到 CGFloat

沒聽懂我在說什麼?其實下面例子中值已經是 CGFloat 了,並且有些括號是多餘的。在清理完這些冗餘之後,編譯時間減少了99.9%。

// 編譯時間: 3431.7 ms
return CGFloat(M_PI) * (CGFloat((hour + hourDelta + CGFloat(minute + minuteDelta) / 60) * 5) - 15) * unit / 180

// 編譯時間: 3.0ms
return CGFloat(M_PI) * ((hour + hourDelta + (minute + minuteDelta) / 60) * 5 - 15) * unit / 180
複製程式碼


Round()

下面是一個很奇怪的例子,下面的例子中變數是一個區域性變數與例項變數的混合。這個問題似乎不是出在四捨五入本身,而是在於結合程式碼的方法。去掉四捨五入的方法大概能減少 **97.6%** 的構建時間。

// 編譯時間: 1433.7ms
let expansion = a — b — c + round(d * 0.66) + e
// 編譯時間: 34.7ms
let expansion = a — b — c + d * 0.66 + e
複製程式碼


注意:以上所有測試都在MacBool Air(13英寸,2013年中)上進行。

嘗試一下吧

不管你是否面臨過編譯時間太長的問題,編寫對編譯器友好的程式碼都是非常有用的。我確信你會在其中找到一些驚喜。作為參考,這裡有完整的程式碼,我的工程中可以5秒內完成編譯…

import UIKit

class CMExpandingTextField: UITextField {

func textFieldEditingChanged() {
    invalidateIntrinsicContentSize()
}

override func intrinsicContentSize() -> CGSize {
    if isFirstResponder(), let text = text {
        let size = text.sizeWithAttributes(typingAttributes)
          return CGSize(width: size.width + (rightView?.bounds.width ?? 0) + (leftView?.bounds.width ?? 0) + 22, height: bounds.height)
        }
        return super.intrinsicContentSize()
    }
}
複製程式碼


相關文章