作者|許斌斌
背景
經過長期的業務迭代,C 端工程增量編譯已經嚴重劣化,2021 年 12 月前,C 端平均增量編譯長達 3 分鐘以上,嚴重影響研發效率,急需優化!經過優化之后,增量編譯時長降低到 2 分鐘左右。
分析
幸福里 app 編譯過程
主要耗時分析
- 全量編譯:pod 編譯占用大部分時間,多達數百秒,CI 打包需要 20 到 30 分鐘。
- 增量編譯:link、資源處理占用大部分耗時(C 端工程優化前該部分占用 130s 耗時)。
方案
LLVM 編譯優化
LLVM 編譯過程
.m 文件編譯從點.o 文件依次經歷以下階段:
- 預處理:去掉注釋、替換宏定義、添加行號和文件標識
- 詞法分析:將代碼切成一個個 token
- 語法分析:驗證語法是否正確,生成語義節點
- 生成 AST:將所有節點組合生成抽象語法樹
- 靜態分析:通過語法樹進行靜態代碼檢測
- 生成 LLVM IR:CodeGen 將語法樹從頂至下遍歷翻譯成 IR 代碼
- 生成匯編:將 IR 代碼轉變成匯編代碼
- 生成目標文件:匯編器將匯編代碼轉變成機器代碼
可以看到,從源文件到目標文件的編譯過程中做了大量工作,如果一個源文件新增了一行代碼,那么所有研發同學 build 時都要按照這些步驟重新走一遍,增加了大量重復耗時。
dolphin 分布式編譯緩存
字節 app infra 團隊通過 hook LLVM Clang,對于基本編譯命令(比如 oc 文件),可以根據內容、依賴將其哈希成一個唯一的 key,我們編譯完新的.m 后,將對應的.o 和 key 存儲在本地硬盤和遠程服務器上,其他研發同學編譯時,就只需要下載.o 文件即可,可以極大提高編譯的效率。幸福里 CI 接入 dolphin 后,打包編譯部分耗時從 600s 降低到 240s。
資源優化
主工程 asset 編譯
主工程資源在每次編譯都會被編譯成 Assets.car,項目里有不少圖片存放在主工程的資源下,每次編譯都會在這一步耗費 30+s,于是將大部分主工程圖片資源遷移至 pod 庫中去,可以降低主工程資源編譯耗時到 5s 內。
copy pods resource
我們工程是用 resources 引用資源,這一步是復制所有 pod 庫的資源并編譯合并到主工程的 Assets.car,耗時大概在 40s 左右。優化有兩個方向:
- 如果改成 resource_bundles,那么每個 pod 都享有自己的 bundle 有自己的 Assets.car,不需要每次都編譯一遍,增量編譯這一步耗時會降低成 0,但是項目改造成本巨大,可當成一個長期目標去做。
- 如果我們不需要 care UI 界面,比如做埋點時,就可以寫腳本在編譯時選擇跳過這一步驟,短期可實現。
link 優化
ld64
ld64 工作原理參考:https://mp.weixin.qq.com/s/tSj6JVEg7plJQm7aDHLyMw
靜態鏈接器 ld64 負責分析 compiler 等模塊輸出的 .o、.a、.dylib、經過對 symbol 的解析、重定向、聚合,組裝出 executable。ld64 主要工作流程如下:
zld
zld 是基于 ld64 開發的優化版鏈接器,增加并發數、使用效率更快的數據結構去優化 link 過程,當然我們也可以參與優化 zld,如飛書一位大佬就通過 map 查找優化線性查找,降低算法時間復雜度優化了符號決議的耗時。
線性查找
map 查找
接入 zld 數據對比
ld64 數據:
zld 數據:
結論
數據對比:
優化前:3.79m
優化后:1.91m
實用技巧
資源拷貝
項目 pod install 時會在 pods-target-resources 生成資源拷貝腳本代碼, 編譯的時候都會運行這個腳本,如果想跳過資源拷貝,直接在 resources 第一行加上 exit 0 即可。
zld 調試
zld 源碼:https://github.com/michaeleisel/zld
使用 zld 編譯工程,查看編譯日志,獲取 link 命令代碼:
刪掉括號和里面的東西,clang 命令后加一個-v,可以顯示 link 參數,然后執行腳本,生成 link 參數,復制并刪除-demangle 之前的東西,存到 juzi.txt:
-demangle -lto_library /Applications/Xcode.app/Content......
打開 zld 工程,編譯模式調整為 release(debug 運行太慢,release 運行快但是不能斷點調試),并將 juzi.txt 的參數復制到 arguments,就可以直接調試項目的 link 過程了。
分析 zld 耗時
將 zld 工程跑出來的 release 版可執行文件復制到桌面。
打開 xcode 的 instruments 的 time profiler,選擇桌面上的 zld 可執行文件。
將 juzi.txt 參數中的\s-換成 \\n-,并復制到上圖的 arguments,然后運行并分析。
-demangle \
-lto_library /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/libLTO.dylib \
-dynamic \
...
如圖,getUserVisibleName()耗時較多,我們查看 zld 源碼:
經過斷點或加日志測試發現,這個方法永遠找不到".llvm."的子串(僅作為 demo 測試),于是嘗試改成以下代碼:
再次編譯產生新的可執行文件,經過 instruments 再次測試得到如下數據:
Todo
- 將 resources 改成 resource_bundles,將資源拷貝耗時真正的降為 0。
- 項目中 swift 用的越來越多,可以接 dolphin 對 swift 的編譯緩存。
- 探索 lld 的行業動態,進一步優化 link 速度。