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
本质特点
-
编译期已知类型
- 调用时 T 是具体类型(Int、Double…)
- 编译器可以 生成特化版本 → 去掉动态派发
- LLVM 可以做 内联、SIMD/vectorization、去掉 CoW 等优化
-
零开销抽象
- 本质上类似 C++ 模板
- 不需要 runtime type metadata
- 不需要额外指针解引用
性能表现
- 高性能,与手写具体类型函数几乎一致
- 开销主要在编译时(代码膨胀,增加二进制大小)
- CoW 容器(数组等)在特化后,runtime check 也可消除
2️⃣ 协议作为参数(Existential / Protocol Type)
func bar(_ x: Numeric, _ y: Numeric) -> Numeric {
return x + y
}
本质特点
-
运行时类型未知
x和y是 协议存在类型(existential type)- 编译器无法在编译期知道具体类型
- 调用操作符(
+)需要 动态派发(witness table)
-
有 runtime 开销
-
包装在 existential container:
- 小型值类型(≤ 3 words)直接存储在 container 内
- 大型值类型/类则使用引用
-
调用协议方法 → 通过 witness table 查找函数指针 → 调用
-
性能表现
- 比泛型慢:每次操作都有间接函数调用
- CoW:protocol type 的数组/字典仍然需要 runtime check
- 不可内联:编译器无法确定具体类型,LLVM 很难做全局优化
3️⃣ 直观对比
| 特性 | 泛型函数 | 协议类型参数 |
|---|---|---|
| 类型解析 | 编译期确定 | 运行期解析 |
| 方法调用 | 静态调用 / 内联 | 动态派发(witness table) |
| CoW 优化 | 可消除 runtime 检查 | 需要 runtime 检查 |
| 内存布局 | 可以在栈上、直接存储 | 被包装在 existential container |
| 编译器优化 | LLVM 可以特化、向量化 | 受限,无法消除动态开销 |
| 二进制膨胀 | 特化版本增大 | 不膨胀(单通用版本) |
4️⃣ 关键结论
-
泛型 = 零开销抽象
- 编译器生成特化版本,性能几乎和具体类型一样
- 适合 高频操作、性能敏感、数组/数字计算
-
协议/存在类型 = 动态抽象
- 灵活、可扩展、易维护
- 运行时需要间接调用,无法消除 CoW 检查
- 适合 接口抽象、模块间解耦、异构类型集合
核心思想:
泛型在编译期确定类型 → 静态分发 → 高性能
协议类型在运行期才确定类型 → 动态分发 → 有开销