Item 44:将与模板参数无关的代码抽离 templates

对齐 Effective C++ 第三版 Item 44:识别 template instantiation 的 code bloat,把 SquareMatrix 的尺寸无关 inversion 算法抽入共同基类,并在二进制体积、常量优化、对象布局与类型安全之间测量权衡。

学习目标

  • 能分析 type 与 non-type template parameters 产生的重复实例化,并用 binary evidence 判断 code bloat
  • 能设计 SquareMatrix common base,把只依赖元素类型的 inversion core 与静态尺寸 wrapper 分离
  • 能比较运行参数、基类状态和保留专用实例三种方案的体积、布局与性能权衡
Template factoring evidence mapinstantiation dimension → binary evidence → shared core + typed wrappernon-type template parameterN = 5 / 10 / 20data-like dimensioncode bloatduplicate symbols / textICache / deploy costcommon base classruntime n + datathin wrapper keeps type safetyparameter-independent code out of templatesstateless corestateful basespecialized fast pathmeasure text + hot path抽离重复控制流,不抽掉行为语义、固定尺寸类型安全或已证明的常量优化
模板抽离的目标是让无关维度共享 heavy core,同时保留 typed wrapper;binary、layout 和 benchmark 共同决定是否值得。

模板体积实验

先预测:这个参数真的改变算法吗?

先预测实例化、对象布局和热点优化的影响,再切换抽离策略查看证据。

观察

SquareMatrix<T, N> 让每个 N 生成 wrapper 和可能重复的 heavy invert;N 是值边界,不一定改变算法结构。

决策

先区分 data-like template dimension 与 behavior-shaping dimension,再决定哪些参数可变成 runtime argument。

当前场景 · non-type template parameter / code bloat

先区分 data-like template dimension 与 behavior-shaping dimension,再决定哪些参数可变成 runtime argument。

从两份几乎相同的 invert 开始

template<class T, std::size_t N>
class SquareMatrix {
public:
    void invert();
private:
    std::array<T, N * N> values_;
};
 
SquareMatrix<double, 5> small;
SquareMatrix<double, 10> large;
small.invert();
large.invert();

invert 完整定义在 class template 中,compiler 通常为 SquareMatrix<double, 5>SquareMatrix<double, 10> 分别实例化函数。算法结构相同,区别主要是 N。

Item 44 的原则是 Factor parameter-independent code out of templates(抽离与模板参数无关的代码)。

先预测:两个 N 不同的 invert 是否一定会被 linker 合并?不能把 optional optimization 当作设计保证,必须查看最终 binary。

实例化不是免费复制

header-only templates 容易让重算法在多个参数组合、translation units 和 build variants 中出现。COMDAT/weak symbols 与 identical code folding 可能消除部分重复,但不同常量嵌入、debug info、exception tables 或 optimization decisions 都可能阻止合并。

nm -S --size-sort app | grep 'SquareMatrix.*invert'
size app

工程判断应记录 symbol 数量、text bytes、链接 map 和真实热点,而不是仅凭 source 看起来重复。

把尺寸从代码维度降为数据

若 inversion algorithm 只需要元素类型 T、尺寸 n 和连续数据 pointer,可把 N 变成 runtime argument。

template<class T>
class SquareMatrixBase {
protected:
    static void invert(std::size_t n, T* data) {
        // one inversion implementation per T
    }
};
 
template<class T, std::size_t N>
class SquareMatrix : private SquareMatrixBase<T> {
public:
    void invert() {
        SquareMatrixBase<T>::invert(N, values_.data());
    }
 
private:
    std::array<T, N * N> values_;
};

现在所有 SquareMatrix<double, N> 共用 SquareMatrixBase<double>::invert,wrapper 仍能提供固定 storage、编译期边界和 N 特定 API。

参数无关不是“完全不含参数”

抽离的判断是算法结构是否因参数改变,而不是源码是否提到参数。N 可作为 loop bound 传给同一算法;T 影响 arithmetic、precision 和 operations,因此 core 仍按 T 模板化。

template<class T, std::size_t N, StorageOrder Order>
class Matrix;

N 可能适合降为 runtime size;T 通常保留;Order 若导致完全不同访问算法,可能保留 specialization。必须逐参数审计。

共同基类可以无状态

上例把 n 与 data pointer 作为函数参数,base 不增加每个 matrix object 的 state。

优点是对象布局仍等于数组 storage,copy/move 不需修复内部 pointer。代价是每次调用传递 n/data,且 runtime n 可能降低 unrolling。

SquareMatrixBase<T>::invert(N, values_.data());

thin wrapper 通常可 inline,参数传递开销往往消失;但 heavy core 继续是单份 out-of-line code。

也可以让 base 保存上下文

template<class T>
class SquareMatrixBase {
protected:
    SquareMatrixBase(std::size_t n, T* data) : n_(n), data_(data) {}
    void invert();
private:
    std::size_t n_;
    T* data_;
};

这种 API 简洁,但每个 object 额外保存 n/pointer;copy/move 后 data_ 必须仍指向新对象自己的 storage,容易产生 self-pointer bug。

对于大量小矩阵,额外 bytes 可能比节省的 text 更贵。对象体积、cache locality 和特殊成员复杂度必须计入。

抽离也可能损失常量优化

N=4 的专用实例可能完全展开 loops、使用固定 SIMD shuffle;把 N 变成 runtime argument 后 optimizer 未必跨 out-of-line boundary 恢复常量。

这形成真实 tradeoff:更少 text 改善 I-cache 与部署体积,专用实例可能提升单次计算。用 benchmark matrix 覆盖常见 N、冷启动、热点 throughput 和 binary size。

type parameters 也会制造可抽离重复

vector<int*>vector<const int*> 和不同 object pointer types 的 storage/move mechanics 很相似。库实现可让 typed wrapper 复用 pointer-sized untyped core。

class RawPointerVectorCore {
protected:
    void pushPointer(void* value);
    void* pointerAt(std::size_t index) const;
};
 
template<class T>
class PointerVector : private RawPointerVectorCore {
public:
    void push(T* value) { pushPointer(value); }
    T* at(std::size_t i) const {
        return static_cast<T*>(pointerAt(i));
    }
};

这种技术必须严守 cv、ownership、alignment 和 alias rules;不能为了减代码体积破坏类型语义。

不要把业务不同强行合并

两个 instantiations 机器码相似,不代表 contract 相同。整数 overflow、floating NaN、complex conjugation、allocator propagation 或 exception guarantee 可能要求不同实现。

抽离前列出 preconditions、postconditions、exception safety 与 precision;共享 core 必须接收足够 policy 信息,而不是偷偷采用一种参数语义。

一套可执行的抽离流程

  1. 从 symbol/map 报告找出大且重复的 template instantiations。
  2. 列出每个 template parameter 对 operations、layout、control flow 的实际影响。
  3. 把 data-like dimension 改为 runtime argument,保留 behavior-shaping dimensions。
  4. 选择 stateless core、stateful base 或非成员 helper,并记录 object layout。
  5. 保留 thin typed wrapper,集中 casts、bounds 和 invariants。
  6. 对比 text size、compile/link time、hot benchmarks 和全部 contract tests。

先预测 5x5 与 10x10 double matrices 会共享哪些 symbols,再比较抽离前后的 map file,避免只看总 executable size 被资源段噪声干扰。

小结

  • template 每组 arguments 都可能生成实体,参数无关的重实现会造成 code bloat
  • non-type parameter 若只提供数据边界,可降为共享 core 的 runtime argument
  • common base 只按真正影响算法的 T 实例化,static wrappers 保留固定尺寸类型安全
  • stateless core 不增加对象布局,stateful base 可能引入额外 state 与 self-pointer 风险
  • 抽离和常量专用优化存在 tradeoff,必须同时测量 binary text 与真实热点
  • type-erased core 可共享相似 type instantiations,但 typed wrapper 必须守住语义

资料与写作方式声明

本章以Effective C++, Third Edition, Item 44权威目录界定学习范围,并结合正文列出的技术资料独立重写;不宣称复现原书正文,也不沿用原作表述。

原作版权归作者与出版社所有;本站原创教学结构与表述仅供学习交流。

名词解释

本章出现的专业名词,用大白话再讲一遍。

code bloat

模板近似机器码重复导致的 text 增长。

non-type template parameter

以值作为模板形参。

template instantiation footprint

具体实例占用的代码和元数据。

identical code folding

链接器合并完全相同函数体的优化。

binary text section

可执行文件的机器指令区域。

common base class

承载跨实例共享重实现的基类。

static thin wrapper

保留静态接口并转发共享 core 的轻层。

data-like template dimension

只改变运行数据边界的模板维度。

behavior-shaping template dimension

改变 operations 或算法的参数。

stateless factoring core

不保存对象上下文的共享算法。

stateful factoring base

保存尺寸和数据位置的共同基类。

embedded self-pointer hazard

特殊成员后内部指针指向旧存储。

constant-size optimization

固定尺寸带来的展开和向量化机会。

instruction-cache benefit

较小 text 提升指令缓存命中。

type-erased implementation core

用统一低层表示共享实现。

typed safety wrapper

恢复静态类型和不变量的薄包装。

parameter-specific semantic contract

参数组合必须保留的行为差异。

template factoring workflow

证据驱动的模板抽离和回归流程。

练习

  1. 问题 1:parameter-independent code out of templates 与 code bloat。 5、10、20 三种 double 尺寸重复生成 invert,仍要保留固定 storage 类型。
  1. 问题 2:non-type template parameter 与 common base class。 对象有百万个,但 invert 调用较少。
  1. 问题 3:common base class 抽离后的 code bloat/热点权衡。 binary 明显缩小,但 SIMD 展开消失。

讨论

评论区加载中…