作為嵌入式工程師的你,也許第一次聽說零成本抽象,一聽覺得很高大上也很迷惑,那么究竟什么是零成本抽象呢?
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)在編譯期生成特化代碼,避免了運行時開銷。這使得可以自由使用泛型而不必擔心性能問題,例如標準庫中的Vec、HashMap等集合類型。
迭代器
Rust 的迭代器是零成本抽象的典型例子。編譯器會將高級的迭代器操作如map、filter等優化為手寫的循環代碼,不會引入額外開銷,同時申明式的編程風格放代碼更加簡潔,安全和高效。
let integers = (0..).map(|x| x * x); // 無限平方序列
let first_five = integers.take(5).collect::>(); // [0, 1, 4, 9, 16]
let numbers = vec![1, 2, 3, 4, 5];
let sum: i32 = numbers.iter()
.map(|x| x * x) // 平方
.filter(|x| x % 3 == 0) // 保留能被3整除的
.sum(); // 求和
println!("{}", sum); // 輸出 35
智能指針
Rust 的引用計數智能指針Rc/Arc在沒有引用時的性能等同于裸指針,不會帶來運行時開銷。對于異步系統或中斷系統,經常會用到智能指針來保證現成安全同時性能開銷最小。
宏
Rust 的宏在編譯期展開,生成的代碼就像手寫的一樣高效。宏可以減少模板化代碼的重復。如下抽象中聲明宏的Copy
trait,在運行時執行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);