Rust 零成本抽象:让程序更简单高效

作为嵌入式工程师的你,也许第一次听说零成本抽象,一听觉得很高大上也很迷惑,那么究竟什么是零成本抽象呢?

Zero-cost abstractions is a concept where you can use higher-level languages without incurring additional runtime cost. Essentially, this means that Rust allows you to write code at a high level of abstraction without sacrificing performance.

什么是零成本抽象(Zero-Cost Abstractions)

零成本抽象是一个概念,您可以使用高级语言而不会产生额外的运行时成本。从本质上讲,这意味着 Rust 允许您在高抽象级别编写代码,而不会牺牲运行性能。简单得说,当解决一个复杂问题时,为了能将问题逻辑简单化,不可避免使用一些抽象的代码时,使用 Rust 的零成本抽象,不会导致最终运行的代码在空间和时间上的损耗和浪费,而代码的可阅读性和逻辑更加简明。

Rust 有哪些场景使用了零成本抽象

泛型和 trait

Rust 的泛型通过单态化(monomorphization)在编译期生成特化代码,避免了运行时开销。这使得可以自由使用泛型而不必担心性能问题,例如标准库中的VecHashMap等集合类型。

迭代器

Rust 的迭代器是零成本抽象的典型例子。编译器会将高级的迭代器操作如mapfilter等优化为手写的循环代码,不会引入额外开销,同时申明式的编程风格放代码更加简洁,安全和高效。

let integers = (0..).map(|x| x * x); // 无限平方序列
let first_five = integers.take(5).collect::<Vec<_>>(); // [0, 1, 4, 9, 16]

let numbers = vec![12345];
let sum: i32 = numbers.iter()
                      .map(|x| x * x) // 平方
                      .filter(|x| x % 3 == 0// 保留能被3整除的
                      .sum(); // 求和
println!("{}", sum); // 输出 35

智能指针

Rust 的引用计数智能指针Rc/Arc在没有引用时的性能等同于裸指针,不会带来运行时开销。对于异步系统或中断系统,经常会用到智能指针来保证现成安全同时性能开销最小。

Rust 的宏在编译期展开,生成的代码就像手写的一样高效。宏可以减少模板化代码的重复。如下抽象中声明宏的Copytrait,在运行时执行copy 动作不会有任何其他结构体之外的额外逻辑,该行为在编译期间即被确认。

#[derive(Copy)]
pub struct SocketAddrV6 {
    addr: Ipv6Addr,
    port: u16,
    flow_info: u32,
    scope_id: u32,
}

零大小类型

Rust 的PhantomData等零大小类型可以用于静态分发,实现零开销的类型包装和抽象。如下定义的结构体被称为零大小的类型,因为它们不包含实际数据。虽然这些类型在编译时像是"真实的"(real) - 你可以拷贝它们,移动它们,引用它们,等等,然而优化器将会完全跳过它们。

// 无论 T 是什么类型,实际大小为多少,HexLiteralVisitor结构体的大小都为 0
struct HexLiteralVisitor {
    _ty: PhantomData,
}


struct Enabled;
// 不论模板参数如何,编译后真正执行的都只有一条代码
//    即:self.periph.modify(|_r, w| w.input_mode().high_z());
pub fn into_input_high_z(self) -> GpioConfig {
    self.periph.modify(|_r, w| w.input_mode().high_z());
    GpioConfig {
        periph: self.periph,
        enabled: Enabled,
        direction: Input,
        mode: HighZ,
    }
}

这些类型状态是一个零成本抽象的杰出案例 - 把某些逻辑行为移到编译时执行或者分析的能力。这些类型状态不包含真实的数据,只用来作为标记。因为它们不包含数据,在运行时它们在内存中不存在实际的表示。

use core::mem::size_of;

let _ = size_of::();    // == 0
let _ = size_of::();      // == 0
let _ = size_of::(); // == 0
let _ = size_of::>(); // == 0

静态分发

Rust支持静态分派,意味着编译器在编译时就能确定函数调用的具体地址,减少运行时开销。这是通过单态化等优化实现的。静态分发的基本思想是:编译器根据函数参数的具体类型,在编译期间生成针对该类型的特化代码,从而避免了运行时的虚函数调用开销。

trait Foo {
    fn foo(&self);
}
struct Bar;
impl Foo for Bar {
    fn foo(&self) {
        println!("Bar::foo");
    }
}
fn static_dispatch(x: T) {
    x.foo(); // 静态分发
}
fn main() {
    let x = Bar;
    static_dispatch(x); // 输出 "Bar::foo"
}

通过静态分发,Rust 避免了虚函数调用的开销,同时也避免了动态分发(dynamic dispatch)带来的额外开销,从而实现了零成本抽象。这使得 Rust 程序即使使用了高级抽象,性能也可以与手写的低级实现相当。

所有权系统

Rust的所有权系统避免了运行时的自动内存管理开销,同时也是实现零成本抽象的基础。总的来说,Rust通过编译器优化、语言特性设计等多方面努力,尽可能在编译期消除抽象带来的开销,使得高级抽象在运行时不会比手写低级代码慢。这使得Rust程序员可以自由使用高级抽象而不必过多考虑性能问题。

// 指定 peter 为 12, i32 类型
let peter = 12
// 新的 peter,变量名字虽一样,但是与上面的已经不是同一个变量了,上面那个 peter 已经被释放
let peter = Peple(peter);


请使用浏览器的分享功能分享到微信等