零成本抽象
问题
Rust 的"零成本抽象"(Zero-Cost Abstractions)是什么意思?
答案
C++ 之父 Bjarne Stroustrup 提出的原则:
What you don't use, you don't pay for. And further: What you do use, you couldn't hand code any better.
Rust 继承了这一理念——高层抽象编译后与手写底层代码一样高效。
核心示例:迭代器
// 高层抽象写法
let sum: i32 = (0..1000)
.filter(|x| x % 2 == 0)
.map(|x| x * x)
.sum();
// 编译后与手写循环性能完全一致
let mut sum = 0i32;
for x in 0..1000 {
if x % 2 == 0 {
sum += x * x;
}
}
编译器通过内联 + 单态化将迭代器链优化为一个简单循环,没有函数调用开销、没有堆分配。
零成本抽象的实现机制
| 机制 | 说明 | 示例 |
|---|---|---|
| 单态化 | 泛型在编译时生成具体类型的代码 | Vec<i32> → 专用实现 |
| 内联 | 小函数直接展开到调用处 | 闭包、迭代器适配器 |
| 所有权 | 编译时决定释放时机,无运行时 GC | Drop 在作用域结束时调用 |
| trait 静态分发 | 泛型约束在编译时解析 | fn foo<T: Display>(t: T) |
对比:有成本的抽象
// ❌ 动态分发:有 vtable 查找开销
fn process(items: &[Box<dyn Processor>]) {
for item in items {
item.process(); // 运行时查表
}
}
// ✅ 静态分发:零成本
fn process<T: Processor>(items: &[T]) {
for item in items {
item.process(); // 编译时已确定
}
}
关键点
零成本 ≠ 零代价。单态化会增加二进制大小(code bloat),这是编译时的代价,但运行时是零成本的。
常见面试问题
Q1: 零成本抽象有什么"代价"吗?
答案:
运行时零成本,但编译时有代价:
- 编译时间增加:单态化生成大量代码
- 二进制体积增大:每种类型组合都有独立实现
- Trait Object 是例外:动态分发有轻微运行时开销,但可避免代码膨胀
Q2: 举例说明 Rust 与 Java/Go 在抽象成本上的区别
答案:
| 特性 | Rust | Java | Go |
|---|---|---|---|
| 泛型 | 单态化,零成本 | 类型擦除,装箱开销 | 字典传递/GCShape |
| 接口调用 | 静态分发为主 | 虚方法表 | 接口值(指针对) |
| 闭包 | 内联展开 | 匿名类对象 | 函数值 + 闭包结构体 |
| 迭代器 | 编译为循环 | Stream 有堆分配 | 无对等抽象 |