優化 Swift 編譯速度

四娘發表於2017-04-30

這兩天 Uber 的開發團隊在一個大會上分享了用 Swift 3 重寫客戶端的過程, 視訊裡介紹了一個很黑科技的技巧, 可以極大地加快編譯速度, 我自己試了一下之後發現確實有效, 但也有小坑, 在這裡跟大家分享一下.

Uber 的開發團隊偶然發現如果把所有 Model 檔案全部合併到一個檔案去編譯, 那編譯時間會從 1min 35s 減少到 17s, 那麼我們如果把所有程式碼檔案都合併到一起, 那就可以極大地優化編譯速度了.

WHO(Whole-Module-Optimization) 也會把檔案合併起來再進行編譯, 實際使用時我們發現編譯雖然快了, 但對於編譯時間的減少還是遠沒有直接把檔案合併到一起那麼有效. 主要原因是因為 WHO 除了合併檔案之外, 還會在預編譯階段做這些事情:

  1. 檢測沒有被呼叫的方法和型別, 在預編譯期去掉它們
  2. 給沒有被繼承的類, 沒有被繼承的方法加上 final 標籤, 給編譯器提供更多資訊, 以便這些方法被優化為靜態呼叫或者是內聯進去

這些優化會對於程式的效率有很大的提升, 但編譯時需要載入更多的 context, 每合併一個檔案, 就會遍歷所有檔案進行一次上面的檢查, 編譯時間會隨著檔案的增多呈指數級增長.

Uber 的團隊發現通過增加一個編譯巨集就可以做到只合並檔案, 而不做優化. 進入工程檔案設定 -> Build Setting -> Add User-Defined Settings, key 為 SWIFT_WHOLE_MODULE_OPTIMIZATION, value 設為 YES, 然後把優化級別設為 None 就可以了.

優化 Swift 編譯速度

那麼問題來了, 為什麼 Swift 的編譯器沒有進行這樣的優化呢?

答案很簡單, 因為這種優化會讓增量編譯的顆粒度從 File 級別增大到 Module 級別.

編譯的過程一般是每一個檔案單獨進行編譯, 然後再連結到一起. 編譯後快取的是連結前的產物, 而把所有檔案都合併到一起再編譯, 快取的就是合併後的檔案編譯後的產物.

只要修改我們專案裡的一個檔案, 想要編譯 debug 一下, 就又得重新合併檔案從頭開始編譯一次, 而不能讀取快取跳過沒有被修改的檔案.

但 pod 裡的庫, storyboard 和 xib 檔案就不會受這個影響, 只是我們修改了檔案之後, 就得整個 module 從頭編譯一遍 (我們的專案也是一個 module, 只是 main 函式位於這個 module 而已, 除此之外跟別的 module 沒有任何本質區別)

所以這個優化手段其實沒想象中那麼有用, 反正打包一般都是 CI 去做, 不在本機, 而日常 debug 都會直接增量編譯. 只有到了 Uber 這種規模的團隊, 每一個 feature branch 都需要到 CI 上打包測試, 使用這種優化手段才比較有現實意義.

非要說對於普通開發者的意義的話, 可能是 flow.ci 那種按照時長計費的方式可以省點錢, 畢竟內部測試的時候對效能要求沒那麼高. (仔細想想, 好像還可以省蠻多錢的, 小型專案幾萬行程式碼使用這種優化的話, 之前編譯一次的費用現在可以用三四次, 而且收益也會隨著專案增大呈指數級增長)


喜歡我寫的文章的話可以關注我的部落格

相關文章