今天會簡單介紹造成 build-time overhead 的可能原因, 和有哪些技巧或是工具能夠幫助我們解決這些問題
下一篇才會實際操作
要提醒的是, 在使用下面的優化方法前, 請先評估專案是否有遇到 build performance 的問題
以及 bottleneck 是在哪裡? 如果是專案架構的問題, 建議優先修改專案架構
當 compiler 在處理 source file (.cpp
) 時, 假如該檔案有包含 headers (#include<>
)
就會去 header search path 尋找該檔案, 並建立諸如 symbol table, Abstract Syntax Tree (AST) 等, 然後進行語義分析, code generation 等等, 直到處理完這個 source file (or Translation Unit), 再對下一個 source file 進行相同的處理
從上面可以看出, compiler 在處理 header files 時做了非~常多的事情
給定一個情境, 假設每個 header file 平均需要處理 t ms, 我們有 N source files, M header files
且每個 source files 都包含這 M 個 header files, compiler 處理完這 N 個 translation units 總共需要多少時間呢?
沒錯, 總共需要 M*N*t ms
! 因為每個 header file 都會被處理 N 次
那我們能夠怎麼加速呢?
Unity build 是一種減少 header files 被處理的次數的技巧
我們可以把這 N 個 source files 合併成一個超~大的 source file
這樣就從 N 個 translation unit 變成 1 個了, 所以 compiler 處理 header files 所需要的時間就會變成 M*1*t ms
! 換言之, 我們可以省下 N 倍的時間❗❗❗
所以對於越複雜的專案 (越多 source files), Unity Build 帶來的改善就會越大
CMake 提供 UNITY_BUILD
target property, 讓我們能夠將該 target 用到的 source files 合併
進而達到上面所說的加速的效果
set_target_properties(<target-name> PROPERTIES
UNITY_BUILD TRUE
)
雖然合併成一個大檔可以減少重複處理 headers 的次數, 但會伴隨著幾個 trade-off
除了 Unity Build, 我們也可以將常用且不常改動的 header files 先 compile 過
target_precompile_headers(<target>
<INTERFACE|PUBLIC|PRIVATE> [header1...]
[<INTERFACE|PUBLIC|PRIVATE> [header2...] ...]
)
target_precompile_headers(<target> REUSE_FROM <other_target>)
就像上面提到過的, 除了使用 Unity Build 來避免重複處理 header files 以外
我們也直接平行處理每個 translation unit, 降低重複處理 header files 造成的影響
每個 build tools 有不同的平行化管理方式, 比如 GNU Make 可以用 -j
搭配 -l
限制平行化數量, 避免 oom
Ninja 則預設就是平行化處理, 並且額外支援 job pools
管理每個任務的平行化程度, 可以做到更細緻的管理, 但比較常用在 linking 的平行化而非 compile, 因為 linking 比較吃記憶體
搭配下面會介紹的 Ccache 可以有不錯的加速
Ninja 的更多細節會在 Day 26 介紹
相較於 Unity Build, 平行化有以下優點
相較於 Unity Build, 平行化有以下缺點
除了重複處理 header files 會造成效能瓶頸以外, 重複 compile 相同的 object files 也會增加不必要的時間浪費
雖然 compiler 會根據 source file 是否改變和 object file 的 timestamp 來決定是否要 re-compile, 但如果 dependency 改變或是僅修改 header file, compiler 不一定能知道需要 re-compile
所以, Compiler Cache 就是用來解決這件事情的工具! Ccache 就是其中一種
Ccache 會 cache 住已經 compile 過的 object files
當 compiler 需要的時候會先去 cache 找, 如果找到了就直接複製該 object file 給 compiler 使用
沒有的話就 re-compile 需要的部分, 並重新加入 cache
CMake 提供 cache variable CMAKE_<LANG>_COMPILER_LAUNCHER
和對應的 target property <LANG>_COMPILER_LAUNCHER
讓我們能夠指定 ccache 作為 compiler 的 driver
find_program(CCACHE_EXECUTABLE ccache)
if(CCACHE_EXECUTABLE)
set(CMAKE_CXX_COMPILER_LAUNCHER ${CCACHE_EXECUTABLE})
endif()
Ccache 的功能請見 官方文件
接下來, 就讓我們建立一個很肥的專案, 來試試每種優化方式分別有多少進步吧