ScyllaDB 编译耗时分析:用 ClangBuildAnalyzer 定位拖慢构建的头文件与模板
【免费下载链接】scylladbNoSQL data store using the Seastar framework, compatible with Apache Cassandra and Amazon DynamoDB项目地址: https://gitcode.com/GitHub_Trending/sc/scylladb
ScyllaDB 的构建时间自项目第一天起就是核心痛点——仓库中的 docs/dev/compilation-time-analysis.md 明确指出,ScyllaDB 的 issue #1 就是关于构建过慢的。在模板密集型 C++ 代码(Seastar 与 ScyllaDB)中,模板实例化与代码生成的耗时远超源码解析,两个等长的头文件可能一个只增加毫秒级开销、另一个却拖慢整秒。本文基于该官方开发者文档,完整讲解如何用 Clang 的-ftime-trace选项配合 ClangBuildAnalyzer 工具,测量、聚合、解读编译时间数据,从而找到最昂贵的头文件与模板实例化,并验证代码修改对构建性能的实际影响。读完本文,你将掌握一套可复制的"构建性能剖析 → 修改 → 验证"的完整工作流。
为什么头文件体积不等于构建开销
文档首先给出了一个关键认知:C 编译器时代,编译时间与源码量基本成正比,减少不必要的头文件包含是唯一手段。但在现代 C++ 中,模板实例化和代码生成的时间开销比单纯解析高出几个数量级。这意味着优化目标不再是"减小头文件",而是"减少昂贵模板的重复实例化次数"以及"打断不必要的间接包含链"。
ScyllaDB 的构建系统正是围绕这一认知设计的。从源码结构看,configure.py 中定义了 dev、release、debug、sanitize 等构建模式,其中 dev 模式的定位就是"optimized for fast build times"(configure.py),它使用-O2、不生成调试信息。而构建脚本在生成 build.ninja 时,针对scylla主程序启用了预编译头(-fpch-instantiate-templates编译stdafx.hh.pch,见 configure.py)——这也是为什么模板开销统计仍会暴露问题:PCH 无法消除模板在不同编译单元中的重复实例化。
安装 ClangBuildAnalyzer
ClangBuildAnalyzer 是一个开源工具,用于聚合所有源文件的-ftime-trace日志,判断哪些头文件和模板最拖慢构建。安装步骤:
git clone https://github.com/aras-p/ClangBuildAnalyzer然后用make -f projects/make/Makefile构建,并将产物build/ClangBuildAnalyzer加入 PATH。注意该工具只依赖 Clang 的 trace 输出格式,不需要修改 ScyllaDB 仓库本身。
开启 -ftime-trace:两种官方入口
文档给出的标准做法是:在 Scylla 工作目录中,先清理旧构建目录,避免把已删除源文件或无关构建模式的旧.json计入统计:
rm -r build然后重新运行configure.py生成 build.ninja,并追加-ftime-trace到编译命令:
./configure.py --disable-dpdk --cflags=-ftime-trace--cflags参数在 configure.py 中定义为 "Extra flags for the C++ compiler",其值最终会进入每个编译模式(dev/release 等)的lib_cflags并透传给 build.ninja 中所有 C++ 编译规则。
此外,仓库还提供了两个更便捷的等价入口,可视为同一机制的封装:
--time-trace参数(ninja 后端):configure.py 中显式定义了--time-trace开关,其帮助文本说明"Each .o produces a .json file analyzable with ClangBuildAnalyzer or chrome://tracing"。在源码内部,当该参数开启时,-ftime-trace同样被追加到user_cflags(configure.py)。因此./configure.py --time-trace与--cflags=-ftime-trace效果一致,且语义更清晰。- CMake 后端:若使用
./configure.py --use-cmake构建,cmake/mode.common.cmake 提供了Scylla_TIME_TRACE选项,用法为cmake -DScylla_TIME_TRACE=ON ...,它强制要求 Clang 编译器,否则直接FATAL_ERROR,并在满足条件时对全部编译添加-ftime-trace。
构建并收集每文件的 JSON trace
开启 trace 后,只需构建单一构建模式的scylla主可执行文件(文档以 dev 模式为例):
ninja build/dev/scylla构建过程中,每个被编译的源文件都会在构建目录下生成一个独立的JSON 文件,包含该文件的编译耗时分析。例如编译cdc/generation.cc会产生build/dev/cdc/generation.json。
这些 JSON 文件采用 Chrome trace 格式,可以单独用各种工具查看(包括 Google 的 Perfetto UI,可通过chrome://tracing加载)。但单文件视角回答不了"整体最慢的是哪个头文件/模板"这个问题——这正是 ClangBuildAnalyzer 的价值所在:
ClangBuildAnalyzer --all build/dev build/dev/clba.bin ClangBuildAnalyzer --analyze build/dev/clba.bin | less--all把所有.json合并为一个二进制文件clba.bin;--analyze则产生人类可读的报告。
迭代使用:修改代码后的正确复测方法
文档给出了四条重要的实操细节,直接影响测量正确性:
- 首次构建会全量重编。因为
-ftime-trace改变了命令行,即使之前用 ccache 构建过,第一次带 trace 的 ninja 也必须全部重编。并且 ccache 在-ftime-trace启用期间会主动放弃缓存(ccache 官方 release notes 中仅说明"too hard")。 - 改动后只需重跑 ninja 与 ClangBuildAnalyzer 两步。ninja 只重编受影响的源文件,这些文件对应的
.json会更新,ClangBuildAnalyzer 会结合新旧 JSON 生成最新的统计。 - 删除源文件要手动清理残留 JSON。被移除源文件的
.json仍留在build/dev/下,会向统计中注入错误信息;需手动删除该 JSON,或删除整个build目录重来。 - 不再需要测量时,去掉
--cflags=-ftime-trace(或--time-trace)重跑 configure.py 即可恢复普通构建。
用 du 估算构建时间收益
文档指出了一个实用的替代指标:构建时间测量精度不高,小幅改进难以直接观测;但 ScyllaDB 团队观察到构建时间与产出的目标文件总大小高度相关:
du -bc build/dev/**/*.o其原理是 C++ 编译器慢通常因为它生成了巨量代码。因此这条du命令的输出可作为"本次修改节省了多少编译工作量"的良好估计量——它比墙钟时间更稳定、可重复。
解读 ClangBuildAnalyzer 报告:六类输出逐段拆解
ClangBuildAnalyzer --analyze build/dev/clba.bin的输出由多个榜单组成,文档按信息价值从高到低逐一解释。以下以文档中的真实输出为例。
1. Files that took longest to parse / codegen
这两个段落列出解析(前端)和代码生成(后端)最耗时的源文件。文档当时的头号问题是messaging_service.cc:
**** Files that took longest to parse (compiler frontend): 84164 ms: build/dev/message/messaging_service.o ... **** Files that took longest to codegen (compiler backend): 135417 ms: build/dev/message/messaging_service.o即仅约 1,376 行代码(当前仓库中 message/messaging_service.cc 为 1,500 余行,与文档描述一致)的源文件,前后端合计消耗约 4 分钟 CPU 时间。文档诚实地指出:这类信息告诉我们"哪个文件慢",却不解释"为什么慢"。它的实用价值有限——最多用于把最慢的文件排在并行构建最前面(避免 straggler),或把大文件进一步拆分。
2. Templates that took longest to instantiate(最有价值的一节)
**** Templates that took longest to instantiate: 385738 ms: fmt::detail::vformat_to<char> (552 times, avg 698 ms) 197016 ms: boost::basic_regex<char>::assign (329 times, avg 598 ms) 195522 ms: ser::deserialize<seastar::simple_memory_input_stream> (1617 times, av g 120 ms) ...这份榜单上的模板有两个共同特征:被很多源文件使用(常因为出现在被广泛包含的头文件中),且单次实例化极其昂贵(上例中vformat_to<char>每次实例化高达 0.7 秒)。针对它们,文档给出三条具体优化路径:
- 减少实例化次数——例如避免在热门头文件中包含该模板,或减少对该头文件的包含;
- 改写模板,减少其内部嵌套的其他模板;
- 使用
extern template显式实例化,让模板只实例化一次。
对比前一小节可以看到差异的本质:单个函数只编译一次,再慢也只是常数;而模板是乘以包含它的编译单元数量——552 次 × 698ms ≈ 386 秒,这就是模板榜单值得优先处理的原因。
3. Template sets that took longest to instantiate
*** Template sets that took longest to instantiate: 640682 ms: std::unique_ptr<$> (31372 times, avg 20 ms) 581171 ms: std::__and_<$> (351772 times, avg 1 ms)"template set"把同一模板族的所有具体化归并统计。文档认为这一节直接可用性较低:我们只能寄望其他改动顺带减少std::unique_ptr<>的实例化次数,没有直接可操作的手段。
4. Functions that took longest to compile
**** Functions that took longest to compile: 2899 ms: cql3_parser::CqlParser::cqlStatement() (build/dev/gen/cql3/CqlParser.cpp) 2144 ms: replica::database::setup_metrics() (replica/database.cc)可以挑个别函数看看为何慢(文档举例:alternator::stats::stats()这样很短的函数因 Boost options 的宏技巧耗时整秒)。但总体上这一节不值得深挖,因为每个函数只编译一次,优化收益有上限——这与模板"被编译数百次"的情形形成鲜明对照。
5. Function sets that took longest to compile / optimize
与上一节类似,但针对模板函数(可被编译多次):
*** Function sets that took longest to compile / optimize: 57092 ms: fmt::v9::appender fmt::v9::detail::write_int_noinline<$>(...) (1071 times, avg 53 ms) 44361 ms: fmt::v9::detail::format_dragon(...) (357 times, avg 124 ms)这一节帮助从"函数体"角度交叉印证模板榜单——例如 fmt 库的格式化路径在这里和模板实例化榜单中反复出现,说明其是构建开销的重灾区。
6. 最昂贵头文件(文档称"最有趣的段落之一")
1086240 ms: replica/database.hh (included 139 times, avg 7814 ms), included via: 47x: query_processor.hh wasm.hh 46x: <direct include> 14x: schema_registry.hh 6x: user_function.hh wasm.hh 5x: cql_test_env.hh 4x: column_family.hh ...该段落的信息密度最高,包含三层内容:
- 总开销:
replica/database.hh累计 1086 秒(当时约占总构建时间 6%),被包含 139 次、平均每次约 8 秒; - 包含链统计:139 个包含者中只有 46 个是直接包含,其余经过
wasm.hh、query_processor.hh、schema_registry.hh等头文件间接引入——这些间接包含是否必要,正是需要逐一审查的对象; - 优化方向:减少
database.hh的包含、拆分它、或让它做的事更少。
注意:头文件开销是高估值
文档特别强调,1086 秒这个数字是过估计。实际删掉database.hh不会减少 1086 秒编译时间,原因是:database.hh包含的其他头文件、实例化的其他模板,会"记账"到这个头文件名下——因为它恰好在某个编译单元中最先使用了这些模板。但其他头文件同样会用到它们,即使删掉database.hh它们照样会被实例化。文档的结论是:不实际动手试一下,无法知道真实收益。因此对榜单应该"排序参考、实测验证",这正是下一节du度量存在的原因。
用 ClangBuildAnalyzer.ini 定制报告
ClangBuildAnalyzer 的每个榜单默认只展示固定数量的 Top N 条目。文档介绍了ClangBuildAnalyzer.ini配置机制,并给出了作者实际使用的完整配置(上游项目也提供了一份示例 ini):
[counts] # files that took most time to parse fileParse = 20 # files that took most time to generate code for fileCodegen = 20 # functions that took most time to generate code for function = 30 # header files that were most expensive to include header = 1000 # for each expensive header, this many include paths to it are shown headerChain = 10 # templates that took longest to instantiate template = 50 # Minimum times (in ms) for things to be recorded into trace [minTimes] # parse/codegen for a file file = 1 [misc] # Maximum length of symbol names printed; longer names will get truncated maxNameLength = 270 # Only print "root" headers in expensive header report, i.e. # only headers that are directly included by at least one source file onlyRootHeaders = false各配置项的含义:
[counts]段控制各榜单展示条数。注意header = 1000——头文件榜单几乎不做截断,因为间接包含链分析需要尽可能完整的数据;headerChain = 10表示每个昂贵头文件展示 10 条包含路径;[minTimes]段控制记入 trace 的最小时间阈值(毫秒),file = 1意味着只要解析/代码生成超过 1ms 的文件都会被记录;[misc]段中maxNameLength = 270限制打印的符号名长度(超长截断);onlyRootHeaders = false表示不仅打印被至少一个源文件直接包含的"根头文件",间接引入的头文件也会出现在榜单中——对于排查wasm.hh、query_processor.hh这类间接包含链,保持 false 是合理的。
小结:从 trace 到优化的完整闭环
将文档内容串起来,ScyllaDB 的编译时间治理工作流是:
- 清理
build目录,用./configure.py --cflags=-ftime-trace(或--time-trace/ CMake 的-DScylla_TIME_TRACE=ON)重新生成构建脚本; ninja build/dev/scylla收集每个源文件的 JSON trace;ClangBuildAnalyzer --all build/dev build/dev/clba.bin合并,--analyze出报告;- 优先处理模板实例化榜单和最昂贵头文件榜单中的项目:减少热门头文件中的模板包含、拆分头文件、审查间接包含链、必要时使用
extern template; - 修改后只重跑 ninja + ClangBuildAnalyzer 两步得到更新统计;
- 由于单次构建时间测量噪声大,用
du -bc build/dev/**/*.o的目标文件总大小作为构建工作量节省量的稳定代理指标。
这套方法的核心洞察在于:对 ScyllaDB 这类模板密集型代码库,"哪个文件慢"远不如"哪个模板被实例化了多少次、哪个头文件通过多少条间接链被包含"来得可操作——而-ftime-trace+ ClangBuildAnalyzer 恰好回答了后两个问题。
【免费下载链接】scylladbNoSQL data store using the Seastar framework, compatible with Apache Cassandra and Amazon DynamoDB项目地址: https://gitcode.com/GitHub_Trending/sc/scylladb
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考