背景与测量口径

  • 目标模块:业务侧的规则引擎,约5k 行 C++,接口稳定,频繁小改动触发重编译。
  • 测量口径:在冷构建下,使用同一提交、同一机器、同一构建命令,取10 次平均;对比“改动前/改动后”的模块级增量构建耗时(非全量)。
  • 基线指标:改动前模块增量构建平均3.2s;改动后0.8s,约4x提速,且接口 ABI 保持兼容。

      7 天重构一个 C++ 模块:三处改动让编译时长从 3.2s 降到 0.8s

      改动一 前置声明 + 轻量头文件,砍掉无用 #include

  • 关键动作
  • 头文件中移除所有“仅用于指针/引用/返回值”的类型定义所对应的#include,改为前向声明(class/struct/enum)。
  • 将仅用于输出的替换为;仅当头文件内出现“按值使用流对象”时才包含完整头。
  • 清理“历史遗留包含”和“间接包含”,用编译报错逐一定位必需头。
  • 影响与收益
  • 头文件从~380 行收敛到~120 行;单个 TU 的解析树显著变小。
  • 模块内所有源文件的前置依赖从平均~45个头下降到~18个,增量构建因“被包含头变更”触发的重编译大幅减少。
  • 可复用模板

      // rules_fwd.h#pragma once#include #include #include #include namespace rules {class Rule;class RuleContext;enum class RuleStatus : uint8_t;struct MatchResult { std::string rule_id; RuleStatus status; double score;};using RulePtr = std::unique_ptr;using ContextPtr = std::shared_ptr;} // namespace rules

      // rules.h#pragma once#include "rules_fwd.h"#include "detail/matcher.h" // 真正需要的实现细节放 cpp/私有头#include "detail/actions.h"namespace rules {class Rule { public: explicit Rule(std::string id); MatchResult Evaluate(ContextPtr ctx) const; // 仅声明,不暴露实现细节private: class Impl; std::unique_ptr impl_;};} // namespace rules

  • 原理依据
  • 前向声明能显著减少头文件传递依赖;提供轻量的流类型前向声明,避免引入庞大的;删除无用包含是业界通用且收益最高的“第一刀”。

      7 天重构一个 C++ 模块:三处改动让编译时长从 3.2s 降到 0.8s

      改动二 Pimpl 化核心类,隔离实现变更

  • 关键动作
  • 将“体积大、依赖多、变更频繁”的成员(如正则、解析器、第三方库的句柄、容器等)移入Impl结构体,头文件仅保留不透明指针(如std::unique_ptr)。
  • 在.cpp中定义 Impl;对外接口保持值语义/const 正确性,避免把实现类型泄漏到头文件。
  • 析构函数与移动操作在头文件中默认或显式默认,在 cpp 中定义,确保符号可见性与 ABI 稳定。
  • 影响与收益
  • 头文件不再依赖等“重头”库;模块对外 ABI 稳定,内部实现可任意演进。
  • 改动实现细节(如替换正则引擎、调整评分策略)不再触发模块外部的重编译,仅影响模块内部1 个 TU。
  • 可复用模板

      // rules.h#pragma once#include "rules_fwd.h"namespace rules {class Rule { public: explicit Rule(std::string id); ~Rule(); // 在 cpp 中定义 Rule(Rule&&) noexcept; Rule& operator=(Rule&&) noexcept; Rule(const Rule&) = delete; Rule& operator=(const Rule&) = delete; MatchResult Evaluate(ContextPtr ctx) const; private: class Impl; std::unique_ptr impl_;};} // namespace rules

      // rules.cpp#include "rules.h"#include "detail/matcher.h"#include "detail/actions.h"namespace rules {class Rule::Impl { public: explicit Impl(std::string id) : id_(std::move(id)) {} MatchResult Evaluate(ContextPtr ctx) const { /* ... */ } private: std::string id_; detail::Matcher matcher_; detail::Actions actions_;};Rule::Rule(std::string id) : impl_(std::make_unique(std::move(id))) {}Rule::~Rule() = default;Rule::Rule(Rule&&) noexcept = default;Rule& Rule::operator=(Rule&&) noexcept = default;MatchResult Rule::Evaluate(ContextPtr ctx) const { return impl_->Evaluate(std::move(ctx)); }} // namespace rules

  • 原理依据
  • Pimpl/PIMPL通过“编译防火墙”将实现细节与接口解耦,显著降低“实现变更 → 外部重编译”的波及面,是大型 C++ 项目常用的结构级优化。

      7 天重构一个 C++ 模块:三处改动让编译时长从 3.2s 降到 0.8s

      改动三 移除不必要的继承,改为组合与接口抽象

  • 关键动作
  • 将“仅为复用代码/拥有权”的继承改为组合;把“行为多态”抽象为纯接口(抽象基类/概念)。
  • 若确需继承,优先private 继承表达“实现继承”,避免对外暴露不必要的接口契约。
  • 影响与收益
  • 头文件减少虚表/基类链的引入,依赖更清晰;模板/内联扩散风险下降。
  • 组合+接口让依赖方向更可控,后续替换实现(如换成 SIMD/多线程评分器)无需改动调用方。
  • 可复用模板

      // rules_fwd.hstruct Evaluator { virtual ~Evaluator() = default; virtual double Score(const MatchResult&) const = 0;};// rules.hclass Rule { public: // 通过接口注入行为,而非继承具体类 explicit Rule(std::string id, std::unique_ptr eval); MatchResult Evaluate(ContextPtr ctx) const; private: class Impl; std::unique_ptr impl_;};

  • 原理依据
  • 不必要的继承会引入“实现耦合”和“头文件膨胀”;通过组合/接口替代,可显著降低编译耦合与维护成本。

      配套工程化动作(让收益稳定落地)

  • 预编译头文件(PCH):将稳定且广泛包含的标准库/第三方头放入pch.h,在关键 TU 中使用,减少重复解析。
  • 编译器缓存:启用ccache/clangd cache,对未变更 TU 直接命中缓存,二次构建常能“秒过”。
  • 并行与资源:合理设置-j并行任务数,使用SSD/NVMe,避免 I/O 成为瓶颈;必要时考虑分布式编译。
  • 度量闭环:在 CI 中加入“模块级增量构建时长”与“头文件包含数/依赖深度”指标,防止回归。
  • 原理依据
  • PCH、ccache、并行/分布式与I/O 优化是工程实践中验证有效的“组合拳”,能在不改业务代码的前提下进一步压缩构建时间。

      7 天重构一个 C++ 模块:三处改动让编译时长从 3.2s 降到 0.8s

      效果与复盘

  • 指标变化
  • 模块增量构建:3.2s → 0.8s(约4x)
  • 头文件包含数:平均~45 → ~18
  • 外部重编译触发:由“实现细节改动”引发的跨模块重编译基本消失
  • 风险与注意
  • Pimpl带来一次间接调用开销与堆分配;对极致性能路径可通过“内联热路径/模板特化”在 cpp 内局部优化。
  • 移动语义/析构需在 cpp 中定义,避免ODR问题;接口变更要控制ABI 兼容(不改类布局、不改虚表结构)。
  • 可复制清单
  • 先清包含、再 Pimpl、后拆继承;每一步都跑一次“模块级增量构建”确认收益与回归。
  • 把“稳定依赖”塞进 PCH,把“易变依赖”挡在模块外;用 ccache 兜底日常开发。

      以上三处改动,分别作用于“头文件依赖”“实现可见性”“类型耦合”,从根源上减少了增量构建的触发面与解析成本。配合工程化手段,能在一周内稳定落地,并把模块级增量构建稳定在亚秒级到 1 秒区间。