多型 Box<dyn Trait>
在 Rust 實現 多型 (Polygmorphism) 最常見的方法就是 Box<dyn Trait>
事實上當我們使用 Box<dyn Trait> 時,Rust 還會做:
- 根據上下文對
Trait加上'static或對應的 lifetime,保證在std::thread::spawn傳遞動態物件不會突然失效 Box<dyn Trait>在記憶體佔兩個指標的空間,資料指標和 vtable 指標(指向一個表格,紀錄該 Trait 所有函式位址)
一個例子如下:
use std::hint::black_box;
use std::time::Instant;
// 定義我們的 Trait
trait Animal {
fn speak(&self) -> i32;
}
// 實作 Cat
struct Cat;
impl Animal for Cat {
#[inline(never)] // 為了公平測試,我們強制不讓編譯器直接把函式內聯化
fn speak(&self) -> i32 {
1
}
}
/// 1. 靜態分派 (Static Dispatch)
/// 編譯器會為每一種具體型別 T 生成一份專屬的函式實作。
fn bench_static<T: Animal>(animal: T, iterations: u32) {
let start = Instant::now();
let mut sum = 0;
for _ in 0..iterations {
// 因為 T 是確定的,這裡沒有 Vtable 查表,直接 Call 函式位址
sum += black_box(animal.speak());
}
println!("Static Dispatch: Result = {}, Time = {:?}", sum, start.elapsed());
}
/// 2. 動態分派 (Dynamic Dispatch)
/// 使用 Box<dyn Trait>,這就是你之前測試的模式。
fn bench_dynamic(animal: Box<dyn Animal>, iterations: u32) {
let start = Instant::now();
let mut sum = 0;
for _ in 0..iterations {
// 透過胖指標尋找 Vtable,再跳轉到函式位址
sum += black_box(animal.speak());
}
println!("Dynamic Dispatch: Result = {}, Time = {:?}", sum, start.elapsed());
}
fn main() {
let iterations = 100_000_000;
println!("--- Rust Performance Comparison ---");
// 測試靜態分派
let cat_static = Cat;
bench_static(cat_static, iterations);
// 測試動態分派
let cat_dynamic: Box<dyn Animal> = Box::new(Cat);
bench_dynamic(cat_dynamic, iterations);
}
/*
cd examples\lifetime\box_dyn_trait
cargo run --bin test --release
Static Dispatch: Result = 100000000, Time = 29.2369ms
Dynamic Dispatch: Result = 100000000, Time = 27.1714ms
*/
而 C 沒有 trait 所以得透過 struct 和 Function Pointers 來模擬
- 定義一個 Vtable struct
- 定義一個 物件 struct,包含資料與指向 Vtable 的指標
並且 C 中我們寫的是 cat.vtable->speak(cat.data) 需要:
- 先讀取
cat.vtable的位址 - 再從該位址讀取
speak函式的位址 - 最後跳轉
這比 Rust 多了一次 記憶體解引用 (Dereference)
#include <stdio.h>
#include <time.h>
typedef struct
{
int (*speak)(void *);
} VTable;
typedef struct
{
void *data;
VTable *vtable;
} DynAnimal;
int cat_speak(void *self) { return 1; }
VTable CAT_VTABLE = {.speak = cat_speak};
int main()
{
DynAnimal cat = {.data = NULL, .vtable = &CAT_VTABLE};
clock_t start = clock();
long long sum = 0;
for (int i = 0; i < 100000000; i++)
{
sum += cat.vtable->speak(cat.data);
}
printf("Result: %lld, C Time: %f s\n", sum, (double)(clock() - start) / CLOCKS_PER_SEC);
return 0;
}
/*
cd examples\lifetime\box_dyn_trait
gcc test.c -O3 -march=native -o test
./test
Result: 100000000, C Time: 0.100000 s
*/
在動態分派的場景下,效能的殺手通常是 間接跳轉 (Indirect Branch)
C 的實作完全依賴函式指標,編譯器很難穿透指標去優化
而 Rust 代碼中,因為 animal 是在 main 函式內直接建立且沒有被修改過的
LLVM 發現雖然寫的是 dyn Animal,但百分之百確定這就是 Cat
於是 LLVM 會嘗試把「查表後跳轉」直接變成「內聯 (Inline)」或者直接呼叫具體函式
這讓 CPU 的分支預測器 (Branch Predictor) 輕鬆許多