編者注:這篇文章釋出與2013年11月4日,那個時候的Xcode設定選項和當前版本多少有些同,原理相同。
寫給沒有耐心的人
如果你不想或者由於時間關係不能讀完整篇文章,那麼你至少應該在你的iOS開發流程(例如未釋出的構建過程中)中使用下面的構建設定。
- 使用普通的“DWARF”而不是“DWARF with dSYMFile”作為你的“Debug Information Format”
- 不要使用 –O4 標識編譯專案程式碼,也不要使用帶有-O4編譯的靜態連結庫。因為這樣Clang會開啟連結時優化(LTO),這會延緩連結速度。最多使用-O3 。這些設定變更是近期才應用到我們自己的iOS程式碼庫中的,效果非常明顯。
問題
上週我參加了Spotify的內部移動訓練營,這是一個為期一週的移動端開發介紹。這個訓練營旨在告知與會者關於app的前沿技術,所以開發者無論有無移動開發經驗,都可以修改程式碼,或者追加新的特徵。我參加了iOS的課程。
訓練營對於像我這種幾乎沒有iOS開發經驗的人來說應該是非常有趣的。然而,這一週課程期間,在等待Xcode完成build的過程中,我經常感到沮喪。每次按下 來測試修改過的內容,看著那幾乎無止境的等待迴圈,我都禁不住沮喪。對於像我這種經常使用試錯法的非專業人士來說,這種狀況特別糟糕。
縮短build時間,似乎是一個有待解決也很有趣的難題,也是一個瞭解OSX內部結構的大好機會。因此,我決定嘗試一下。非常幸運,取得了顯著效果:我減少了50%的等待時間。
解決方案
我的開發流程(也是最常見的一種情況)如下:
- 修改一些原始檔。
- 按下Xcode中的 按鈕。
- 觀察在phone或者模擬器上的效果(我的例子中使用的是模擬器)。
- 跳到第一步。在我修改了一個Spotify的iOS客戶端中相對較小的 Objective C 原始檔之後,我記錄了一下步驟(2)到步驟(3)花費的時間,直到模擬器載入完應用程式:我的家用iMac(說實話,已經很舊了)花費了82秒(平均值)。通過觀察Xcode的編譯流程我意識到大部分時間花費在“Linking”和“Generating dSYM file”階段。
在命令列中進行一些測量證實了這一點,平均而言:
- Linking花費了29秒
- 生成dSYM 花費了25秒這兩個階段佔用了等待時間的(29 + 25) / 82 * 100 = 62 % 。但是,畢竟,Spotify的iOS客戶端程式碼庫是非常大的(連結器要把大約2000個目標檔案組合起來),花費這麼多時間或許也有些道理。然而,並非完全如此……
dSYM 檔案生成
老實說我對dSYM bundles瞭解不多,只知道其中包含除錯資訊。得知dSYM最初是作為Apple的一個“臨時解決方案”,我感到非常驚訝。事情是這樣的:在OSX早期階段,Apple為了避免在連結器中引入DWARF支援的麻煩,建立了獨立的連結器(dsymutil),這個連結器將除錯資訊從目標檔案中取出,放到一個同一個地方:一個dSYM bundle中。
dSYM bundles 對於發行版本有用,但在開發過程中並不需要。偵錯程式可以從中間目標檔案中獲取除錯資訊,這些檔案在 build 完成後仍然存在。
Xcode 允許開發者設定自己工程的 “Debug Information Format”,可以選擇使用“DWARF”而不是“DWARF 和 dSYM File”。
由此,在XCode中簡單地改變一個選項,就可以減少大約25秒等待時間:非常棒!
Linking
不幸的是,沒有神奇的選項可以跳過連結。或許,你也能跳過它,但是你最終會得到一大堆無用的目標檔案:)
最初,我認為縮短30秒連結時間(像永恆那麼久!)的唯一途徑是研究Apple的連結器,Id64,實現增量連結。這項任務相當複雜,所以我決定首先 profile 一下 Id64 的執行過程來尋找已有的實現方案。
Apple提供了Id64的C++原始碼,儘管是過時版本,我還是嘗試了一下,希望和XCode中包含的最新版本之間不要有根本性的差別。原始碼不是編譯好即時可用的,我參照Macports的程式碼,快速地在 instrument 中執行起來Instruments)。
上面的截圖,也許你一眼看上去並不能獲得太多資訊,但是如果你讀過Id64的設計文件,你會迅速得出:ld::tool::Resolver::resolve()相當於Id64管道的Resolving 階段。簡言之,這個階段負責將表示各個目標檔案的圖表放到一個更大的全域性表裡面。
早些時候已經完成了目標檔案各自圖表的載入,因此,原則上,解析不會太費時,當然不應該影響執行時間(佔執行時間的76.9%)。因此,如果你仔細觀察,你會發現ld::tool::Resolver::linkTimeOptimize()佔用了51.3%的連結時間。
linkTimeOptimize()執行連結時優化(LTO)。在Clang/LLVM領域,這意味著連結器獲得的是LLVM位元組碼,而不是通常的目標檔案。這些位元組碼在一種更抽象的層次上代表程式的執行過程,允許LTO得以進行,但是壞處是,仍然需要將他們轉換成機器程式碼,在連結時需要額外的處理時間。
在Id64加入了一些logging做了一些枯燥的探索,我成功定位那些令人生厭的二進位制檔案。這些二進位制檔案是一個靜態庫的一部分。而這個靜態庫,出於善意,編譯優先順序設定為-O4。 Quoting Clang手冊記述如下:
-O4能夠優化連結時間;目標檔案以LLVM二進位制檔案格式儲存,在連結期間,優化了整個程式。
我將優先順序標誌降為-O3,然後連結器速度提高了51%,每個週期節約了大約15秒。
成果
在我的舊機器上,每個週期大約縮短了25 + 15 = 40秒(佔等待時間的40 / 82 * 100 = 49 %)。我們大膽估算一下大約能節省多少時間:
假設:
- 由於使用新的硬體,效率提高了2倍。
- 開發者每10分鐘一個Edit-Build-Test週期。
- 開發者每天工作5個小時。這樣每人每天可以節省 40 秒/ 2 (5 60 / 10) = 600 秒 = 10 分鐘。在一些大的 iOS 團隊像 Spotify,每10個iOS開發者,每天將會節省1小時40分鐘,完全不需要花費任何代價(儘管我花費幾小時找出這些,但是可以忽略不計).這只是粗略估計,我們也要有所保留。我並沒有打算建立一個嚴格的測試對比。
總結
優化build過程真的是值得的。只需要偶爾花費一些時間,理論上每幾個月花費幾個小時就夠了。這樣做節約了大量時間和金錢。然而,最重要的是,使得開發者更開心。
未來工作展望
進一步優化連結階段是一個難題,因為不同於編譯大量的原始檔,這個過程不是並行的。但是,正如我前面提到的那樣,增量連結是一個眾所周知的好方法。Google的Gold連結器)支援增量連結,並且有完善的說明文件(例如可以看這個視訊),但不幸的是,它目前不支援Mach-O並且以後也不太可能支援。下個內部黑客周,如果那時Apple釋出了最新版的原始碼,我可能會花費那段時間去研究Id64的增量連結。
改善Edit-Build-Test之外的工作流程也很有意義。一些開發者經常改變分支程式,每次都需要全部重新build。不幸的是,Apple從XCode 4.3 開始不再支援分散式構建。我之前已經談論過建立一個 Cocoa Pods 的二進位制版本,甚至一個利用git雜湊的object/library storage源來,但現在我們只能期待他們會成功。
最後同樣重要的是,我們看一下 Android 的構建流程可以如何優化也是很有趣的。