iT邦幫忙

2023 iThome 鐵人賽

DAY 24
0
Software Development

30 天 CMake 跨平台之旅系列 第 24

[Day 24] 你需要再快一點! Build Performance (一)

  • 分享至 

  • xImage
  •  

本日內容

  • Bottleneck: Processing Headers
  • Unity Build
  • Precompile Headers
  • Parallelism
  • Bottleneck: Recompiling Object Files
  • Ccache
  • 預告

今天會簡單介紹造成 build-time overhead 的可能原因, 和有哪些技巧或是工具能夠幫助我們解決這些問題
下一篇才會實際操作

要提醒的是, 在使用下面的優化方法前, 請先評估專案是否有遇到 build performance 的問題
以及 bottleneck 是在哪裡? 如果是專案架構的問題, 建議優先修改專案架構

Bottleneck: Processing Headers

當 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 次

那我們能夠怎麼加速呢?

Unit Build

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

  1. 記憶體用量顯著增加
  2. Re-compile 的次數和量增加, 以前可能只是其中一個 source file 需要重新 compile, 現在因為合併成了一個大 source file, 造成整個大檔都要重新 compile, 反而造成其他未被改動的 source files 也被 re-compile
  3. 平行化程度下降, 或許我們可以不用合併成一個大檔, 而是平行處理每個 source file, 就可以減少不必要的 re-compile, 當然, 需要考量機器的核心數, 平行化和 unity build 同時使用可能有奇效

Precompiled Headers

除了 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>)

優點

  • 省下 runtime (指 compiler 處理 header files 時) 讀取和 compile header files 的時間
  • 減少記憶體用量 (header files 越大越顯著)

缺點

  • 無法跨平台, 跨平台時一樣需要重新 compile
  • 第一次 compile 需要比較久
  • 因為會產生額外的 compile 過的 header files, 需要額外硬碟空間 (現在應該沒人在意就是了:p)

Parallelism

就像上面提到過的, 除了使用 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, 平行化有以下優點

  • 因為每個 translation unit 是單獨的 process 除理, 可以避免佔用大量記憶體
  • 可能 比 Unity Build 更快, 擴展性更好

缺點

相較於 Unity Build, 平行化有以下缺點

  • Dependency 的設計需要允許平行化, 會變得更複雜

Bottleneck: Recompiling Object Files

除了重複處理 header files 會造成效能瓶頸以外, 重複 compile 相同的 object files 也會增加不必要的時間浪費

雖然 compiler 會根據 source file 是否改變和 object file 的 timestamp 來決定是否要 re-compile, 但如果 dependency 改變或是僅修改 header file, compiler 不一定能知道需要 re-compile

所以, Compiler Cache 就是用來解決這件事情的工具! Ccache 就是其中一種

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 的功能請見 官方文件

優點

  • 支援跨平台
  • Dependency detection

缺點

  • 因為會 cache object files, 所以很佔用硬碟空間
  • Cache 是基於 compiler options 和 build system, 如果跨平台時用錯了 build system, 會導致奇怪的錯誤

預告

接下來, 就讓我們建立一個很肥的專案, 來試試每種優化方式分別有多少進步吧


上一篇
[Day 23] 讓別人 Link 我的 Project
下一篇
[Day 25] 你需要再快一點! Build Performance (二)
系列文
30 天 CMake 跨平台之旅30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言