
1. 1MHz inner loop 里的 vtable 税
点云滤波链每点一次虚函数,perf 里虚表间接跳转占可见比例——Release 下更难内联,整条 pipeline 被一次间接调用拖住。CRTP 把多态静态化:编译期知道派生类型,实现函数可内联进热点循环。代价是类型在编译期固定,不能运行时换实现;别把 CRTP 当运维热插拔方案,那是虚函数工厂的职责。若算法族在出厂时已固定、现场不需要换滤波器,CRTP 是合理的零开销抽象;若产品要求插件化换算法,边界仍须虚接口或 C ABI。
2. 基本形态与构造陷阱
template <typename Derived>
struct FilterBase {
void apply(std::span<float> z) {
static_cast<Derived*>(this)->apply_impl(z);
}
};
struct VoxelDownsample : FilterBase<VoxelDownsample> {
void apply_impl(std::span<float> z) { /* inline-friendly */ }
};基类构造函数里若调用派生类方法,向下转型有效但派生成员尚未初始化——基类构造阶段不要派发。CRTP 基类别放虚析构——没有虚表;若 pipeline 需多态销毁,边界仍用智能指针包一层虚接口。误在基类析构期待虚函数行为,逻辑错且无编译器提醒,这类 bug 只在特定析构顺序下暴露。
3. 适用边界
适合算法族固定、编译期选型、机载已知 pipeline、头文件内算法链内部链接。不适合跨动态库插件边界——实现必须在宿主编译可见的头文件里,否则无法实例化派生类。pipeline 内部用 CRTP 链接;边界 export 虚工厂给插件。与类型擦除分界:CRTP 用于 onboard 已知链;插件边界仍 C ABI 或虚接口。概念约束可约束实现函数签名,错误信息比 SFINAE 模板墙可读。
4. 代价:二进制与编译防火墙
每个派生类实例化一份基类代码——二进制体积涨。改一个体素滤波实现全量重编;CRTP 实现必须在头文件,没有编译防火墙,改一行模板 全树重编。运行时换滤波器走 factory 加虚函数;编译期固定链用类型组合 typedef。多重 CRTP 注意菱形继承——通常用 mixin 模板而非多重 CRTP 基类。实例化次数要可控,十个派生各一份大模板可能拖垮 I-cache。
5. 与虚函数多态的取舍
虚函数多态每次调用一次虚表间接,在一兆赫兹点云内层循环里可能可见。CRTP 换的是编译期已知类型加内联整条调用链。若不需要运行时插件替换,CRTP 是零开销抽象;若需要动态库热插拔,CRTP 不能跨 DSO——消费方看不到模板实现。常见分层:内部 CRTP 链拼 pipeline,最外层虚指针给配置与生命周期管理。
6. 失败症状
在插件动态库里继承 CRTP 基类会链接失败或行为未定义。二进制体积涨后 I-cache 压力上升。debug 构建可 typedef 链验证类型闭合;release 对比虚函数版 p99 延迟,确认收益大于体积成本。误把 CRTP 当继承用,在基类析构调「多态」行为——没有虚表,逻辑错。
7. 案例:滤波链改 CRTP 后编译时间翻倍
某点云栈把五级滤波全改成 CRTP,热点从虚表间接降到可内联,单帧 decode 降百分之八,但改一行体素参数全仓库重编四十分钟,迭代节奏被拖死。最后保留内部三级 CRTP、边界一级虚接口,编译时间与性能折中。教训:CRTP 的收益在 hot loop,代价在编译防火墙——团队要接受「改算法 = 全量编」或拆成独立静态库模块。
8. 验收
- 反汇编对比 CRTP 与虚函数实例化次数与 call 指令。
- 同 bag slice benchmark:标量 vs CRTP vs 虚函数,p99 与 cycles。
- 插件动态库不继承 CRTP 基类——code review grep 违规。
- 改派生类后全量编译时间可接受,或已模块化隔离。
CRTP 是零开销抽象的一种——灵活性换速度,hot loop 内用,运维热插拔点仍虚函数包一层。
相关
也可以看看
johan's blog