5-11.【性能分析与优化】泛型函数和使用协议作为参数,在性能上的本质差异是什么?

0 阅读2分钟

1️⃣ 泛型函数(Generic Function)

func foo<T: Numeric>(_ x: T, _ y: T) -> T {
    return x + y
}

let a = foo(1, 2)    // T = Int
let b = foo(1.0, 2.0) // T = Double

本质特点

  1. 编译期已知类型

    • 调用时 T 是具体类型(Int、Double…)
    • 编译器可以 生成特化版本 → 去掉动态派发
    • LLVM 可以做 内联、SIMD/vectorization、去掉 CoW 等优化
  2. 零开销抽象

    • 本质上类似 C++ 模板
    • 不需要 runtime type metadata
    • 不需要额外指针解引用

性能表现

  • 高性能,与手写具体类型函数几乎一致
  • 开销主要在编译时(代码膨胀,增加二进制大小)
  • CoW 容器(数组等)在特化后,runtime check 也可消除

2️⃣ 协议作为参数(Existential / Protocol Type)

func bar(_ x: Numeric, _ y: Numeric) -> Numeric {
    return x + y
}

本质特点

  1. 运行时类型未知

    • xy协议存在类型(existential type)
    • 编译器无法在编译期知道具体类型
    • 调用操作符(+)需要 动态派发(witness table)
  2. 有 runtime 开销

    • 包装在 existential container

      • 小型值类型(≤ 3 words)直接存储在 container 内
      • 大型值类型/类则使用引用
    • 调用协议方法 → 通过 witness table 查找函数指针 → 调用

性能表现

  • 比泛型慢:每次操作都有间接函数调用
  • CoW:protocol type 的数组/字典仍然需要 runtime check
  • 不可内联:编译器无法确定具体类型,LLVM 很难做全局优化

3️⃣ 直观对比

特性泛型函数协议类型参数
类型解析编译期确定运行期解析
方法调用静态调用 / 内联动态派发(witness table)
CoW 优化可消除 runtime 检查需要 runtime 检查
内存布局可以在栈上、直接存储被包装在 existential container
编译器优化LLVM 可以特化、向量化受限,无法消除动态开销
二进制膨胀特化版本增大不膨胀(单通用版本)

4️⃣ 关键结论

  1. 泛型 = 零开销抽象

    • 编译器生成特化版本,性能几乎和具体类型一样
    • 适合 高频操作、性能敏感、数组/数字计算
  2. 协议/存在类型 = 动态抽象

    • 灵活、可扩展、易维护
    • 运行时需要间接调用,无法消除 CoW 检查
    • 适合 接口抽象、模块间解耦、异构类型集合

核心思想:
泛型在编译期确定类型 → 静态分发 → 高性能
协议类型在运行期才确定类型 → 动态分发 → 有开销