iT邦幫忙

2023 iThome 鐵人賽

DAY 25
0
Software Development

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

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

  • 分享至 

  • xImage
  •  

本日內容

  • 專案架構
  • Unity Build
  • Precompiled Headers
  • Ccache
  • Parallelism
  • 預告

Day 25 - Colab

Day 24 介紹了幾種優化 build process 效率的方式
雖然理論上是有用的, 但是口說無憑, 今天就來實際看看那些優化方式能加速多少吧!

專案架構

為了達到測試的目的, 我們寫一個自動產生 headers, source files, executables 的 script generate-sources.py

#!/usr/bin/env python3

import argparse

LIB_DIR = "src/lib"
INCLUDE_DIR = "src/include"
BIN_DIR = "src/bin"

def generate_headers(nfunc: int, id: int):
    dummy_functions = [f"void foo{id}_{i}();" for i in range(nfunc)]
    dummy_functions = "\n".join(dummy_functions)
    with open(f"{INCLUDE_DIR}/header{id}.h", "w") as f:
        f.write(f"#pragma once\n{dummy_functions}")
    return;


def generate_sources(nfunc: int, id: int):
    dummy_functions = [f"void foo{id}_{i}() {{ return; }}" for i in range(nfunc)]
    dummy_functions = "\n".join(dummy_functions)
    with open(f"{LIB_DIR}/header{id}.cpp", "w") as f:
        f.write(f"#include <header{id}.h>\n\n{dummy_functions}")
    return;


def generate_binary(n: int, id: int = -1):
    include_headers = [f"#include <header{i}.h>" for i in range(n)]
    include_headers = "\n".join(include_headers)
    call_funcs = [f"foo{i}_0();" for i in range(n)]
    call_funcs = "\n".join(call_funcs)
    filename = f"{BIN_DIR}/main.cpp" if id < 0 else f"{BIN_DIR}/main_{id}.cpp"
    with open(filename, "w") as f:
        f.write(f"{include_headers}\n\nint main() {{\n{call_funcs}\nreturn 0;\n}}")
    return;


def create(n: int, nfunc: int, nexec):
    generate_binary(n)
    for i in range(n):
        generate_headers(nfunc, i)
        generate_sources(nfunc, i)

    for i in range(nexec):
        generate_binary(n, i)
    return;


def main():
    parser = argparse.ArgumentParser(description="Generate source files")
    parser.add_argument("n", type=int, help="Number of files to generate")
    parser.add_argument("nfunc", type=int, help="Number of functions to generate")
    parser.add_argument("nexec", type=int, help="Number of executables to generate")
    args = parser.parse_args()
    create(args.n, args.nfunc, args.nexec)

if __name__ == "__main__":
    main()

用法如下

./generate-sources.py -h

usage: generate-sources.py [-h] n nfunc nexec

Generate source files

positional arguments:
  n           Number of files to generate
  nfunc       Number of functions to generate
  nexec       Number of executables to generate

options:
  -h, --help  show this help message and exit
  • n
    • Headers 和對應實作的檔案的數量
  • nfunc
    • 每個 header file 中 function 的數量
  • nexec
    • nexec + 1 個 Executables

舉例來說, ./generate-sources.py 5 5 5 會產生

  • 5 個 header files
  • 5 個 source files
  • 每個 header file 中有 5 個 functions
  • 6 個 executables

如圖所示
https://ithelp.ithome.com.tw/upload/images/20230925/20161950mhQGXH5xn9.png

當然, 為了凸顯優化前後的速度差異, 我們會用以下配置

  • 50 個 headers 和 sources
  • 每個 header 有 10 個 functions
  • 6 個 executables

並用 Linux 原生的指令 time 來測量 build process 所花費的時間
比如

# setup projects
./generate-sources.py 50 10 5

# configure project
cmake -S . -B build -DCMAKE_UNITY_BUILD:BOOL=ON

# estimate elapse time
time cmake --build build

那就讓我們開始吧!!!

Unity Build

我們只要在 configure 時加上 cache variable CMAKE_UNITY_BUILD 即可

cmake -S . -B build -DCMAKE_UNITY_BUILD:BOOL=ON
real user system
w/o unity build 0m9.456s 0m5.954s 0m3.267s
w/ unity build 0m2.216s 0m1.476s 0m0.655s

單單只是加了個 cache variable, 總時間就快了 76.5%!

Precompiled Headers

我們可以用 target_precompile_headers 來產生 pch (precompiled header) 檔案

file(GLOB _headers ${CMAKE_SOURCE_DIR}/src/include/*.h)
target_precompile_headers(Main PRIVATE ${_headers})

可以在 build/src/bin/CMakeFiles/Main.dir 底下看到有了 cmake_pch.hxx 等一系列檔案

real user system
w/o pch 0m8.644s 0m5.194s 0m3.135s
w/ pch 0m9.581s 0m5.924s 0m3.388s

由於 precompiled header 第一次多了產生 pch 的時間, 反而比原本沒有 precompiled header 還慢
但不確定是否是專案設計有誤, 即使第二次用了第一次先 compile 好的 headers, 差異也不明顯

real user system
w/o pch 0m0.252s 0m0.141s 0m0.086s
w/ pch 0m0.262s 0m0.153s 0m0.093s

Ccache

我們需要先安裝 ccache

apt install ccache

然後我們先用 ccache -sv 來看一下目前的 cache 狀況

Summary:
  Cache directory:  /root/.cache/ccache
  Primary config:   /root/.config/ccache/ccache.conf
  Secondary config: /etc/ccache.conf
  Stats updated:    never
  Hits:                0 /    0
    Direct:            0 /    0
    Preprocessed:      0 /    0
  Misses:              0
    Direct:            0
    Preprocessed:      0
Primary storage:
  Hits:                0 /    0
  Misses:              0
  Cache size (GB):  0.00 / 5.00 (0.00 %)
  Files:               0

可以看到都是空的, 完美!

然後我們在 src/bin/CMakeLists.txt 中設定 CMAKE_CXX_COMPILER_LAUNCHERccache

find_program(CCACHE_EXECUTABLE ccache)
if(CCACHE_EXECUTABLE)
  set(CMAKE_CXX_COMPILER_LAUNCHER ${CCACHE_EXECUTABLE})
endif()

來 build 一次看看效果

real user system
w/o ccache 0m8.644s 0m5.194s 0m3.135s
w/ ccache 0m11.964s 0m6.864s 0m4.645s

嗯? 怎麼看起來沒有效果呢? 我們再來看看 cache 的狀況

Summary:
  Cache directory:  /root/.cache/ccache
  Primary config:   /root/.config/ccache/ccache.conf
  Secondary config: /etc/ccache.conf
  Stats updated:    Mon Sep 25 13:30:32 2023
  Hits:                0 /  306 (0.00 %)
    Direct:            0 /  306 (0.00 %)
    Preprocessed:      0 /  306 (0.00 %)
  Misses:            306
    Direct:          306
    Preprocessed:    306
Primary storage:
  Hits:                0 /  612 (0.00 %)
  Misses:            612
  Cache size (GB):  0.00 / 5.00 (0.05 %)
  Files:             612

可以看到 Stats 已經被更新過了, 且 cache files 也多了 612 個! 但是同樣也 miss 了 612 次
這是因為我們才第一次 build, 所以還沒有 cache 能用
現在有了之後我們再 build 一次試試吧

real user system
w/o ccache 0m8.644s 0m5.194s 0m3.135s
w/ ccache 0m2.504s 0m1.288s 0m1.121s

可以看到這次的速度非常明顯提升了, 總共快了 71%!

再來看看 cache 有什麼變化

Summary:
  Cache directory:  /root/.cache/ccache
  Primary config:   /root/.config/ccache/ccache.conf
  Secondary config: /etc/ccache.conf
  Stats updated:    Mon Sep 25 13:34:29 2023
  Hits:              306 /  612 (50.00 %)
    Direct:          306 /  612 (50.00 %)
    Preprocessed:      0 /  306 (0.00 %)
  Misses:            306
    Direct:          306
    Preprocessed:    306
Primary storage:
  Hits:              612 / 1224 (50.00 %)
  Misses:            612
  Cache size (GB):  0.00 / 5.00 (0.05 %)
  Files:             612

嗯...cache hits 上升到 50% 了, 當然, 這是因為第一次的 cache miss 也被算進去了, 所以不是很準確, 我們先 ccache -z 重置統計數字後再試試

Summary:
  Cache directory:  /root/.cache/ccache
  Primary config:   /root/.config/ccache/ccache.conf
  Secondary config: /etc/ccache.conf
  Stats updated:    Mon Sep 25 13:37:55 2023
  Hits:              306 /  306 (100.0 %)
    Direct:          306 /  306 (100.0 %)
    Preprocessed:      0 /    0
  Misses:              0
    Direct:            0
    Preprocessed:      0
Primary storage:
  Hits:              612 /  612 (100.0 %)
  Misses:              0
  Cache size (GB):  0.00 / 5.00 (0.05 %)
  Files:             612

因為我們並沒有改變任何的 code 或是 dependency, 所以這次很明顯就能看出 cache hits 是 100% 了🎉🎉🎉

Parallelism

最後來看看平行化, 我們用 nproc --all 來取得最大核心數, 作為平行處理的 job 數量 (我的 Colab session 只有 2 核QQ)

time cmake --build build -j $( nproc --all )
real user system
w/o parallelism 0m8.644s 0m5.194s 0m3.135s
w/ parallelism 0m7.404s 0m6.928s 9m3.747s

由於核心數比較少, 改善效果比較不明顯, 僅提升了約 14.3%

預告

效能的部分到這裡告一個段落, 下一篇要來認識一下新的 generator: Ninja!


上一篇
[Day 24] 你需要再快一點! Build Performance (一)
下一篇
[Day 26] Ninja
系列文
30 天 CMake 跨平台之旅30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言