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