在这里插入图片描述


读完本文,你不仅能用 Option/Result 写出零运行时开销的健壮代码,还能把它们拆成 LLVM IR,看到“零成本”三个字的真正含义;更重要的是,你会明白在什么场景下应该手写 enum,什么时候大胆交给编译器。🦀


1. 开场:为什么 Option/Result 能“零成本”?

在 C/C++ 里,表达“可能失败”通常有两条路:

  1. Sentinel 值(nullptr-1errno)。
    代价:调用者忘记检查 → 未定义行为。
  2. 额外指针 + 动态分配(std::variant + std::error_code)。
    代价:至少一次内存分配 + 分支预测失败。

Rust 用 Option<T>Result<T, E> 把“有无”与“成败”提到类型系统层面,却承诺 零运行时开销。这不是魔法,而是 基于代数数据类型 + 枚举布局优化(niche optimization) 的编译器级协作。


2. 解剖布局:从内存到 LLVM IR

2.1 基本尺寸

use std::mem::{size_of, align_of};

fn main() {
    println!("Option<i32>   : size={}, align={}", size_of::<Option<i32>>(), align_of::<Option<i32>>());
    println!("Result<i32, i8>: size={}, align={}", size_of::<Result<i32, i8>>(), align_of::<Result<i32, i8>>());
}
类型sizealign
i3244
Option<i32>84
Result<i32,i8>84

Option<i32> 只需要 8 字节:4 字节存 i32,4 字节存 discriminant(tag)。
但注意:并不是所有 Option<T> 都会多 4 字节

2.2 niche optimization:把 tag 塞进无效位

&TNonNull<T>fn() 等类型都有一个“无效位”(例如 0x0 地址)。
编译器会把 None 直接编码成这个无效位,于是:

assert_eq!(size_of::<Option<&i32>>(), size_of::<&i32>());
assert_eq!(size_of::<Option<fn()>>(), size_of::<fn()>());

LLVM IR 视角:

; Option<&i32> 在 LLVM 中就是一条指针
; None 用 null 表示,Some(&v) 用非 null 表示
; 判别式完全消失

这就是“零成本”的核心:没有额外内存、没有额外分支


3. 零成本实战 1:用 Result 替代异常

3.1 传统 C 的错误码 vs Rust 的 Result

假设写一个 parse_u8

// C 版本
int parse_u8(const char *s, uint8_t *out) {
    char *end;
    long v = strtol(s, &end, 10);
    if (*end || v < 0 || v > 255) return -EINVAL;
    *out = (uint8_t)v;
    return 0;
}

调用者必须检查返回值,且无法链式组合。

Rust 零成本版本:

#[inline]
pub fn parse_u8(s: &str) -> Result<u8, &'static str> {
    s.parse::<u8>().map_err(|_| "invalid digit or overflow")
}

编译后(release):

example::parse_u8:
    ; 实际上直接调用 core::str::<impl FromStr>::from_str::<u8>
    ; 失败时返回 Err,成功返回 Ok
    ; 无 panic、无 alloc

3.2 链式计算:no_std 下的矩阵乘法

我们写一个 #![no_std] 的矩阵乘法库,每一步都可能因维度不匹配而失败,但要求 零堆分配

#![no_std]
pub struct MatRef<'a, T> {
    data: &'a [T],
    rows: usize,
    cols: usize,
}

#[derive(Debug)]
pub enum MatError {
    MismatchedDimension,
}

impl<'a, T> MatRef<'a, T> {
    pub fn new(data: &'a [T], rows: usize, cols: usize) -> Result<Self, MatError> {
        if data.len() != rows * cols {
            return Err(MatError::MismatchedDimension);
        }
        Ok(Self { data, rows, cols })
    }

    pub fn mul(&self, rhs: &Self) -> Result<heapless::Vec<T, 64>, MatError>
    where
        T: Copy + Default + core::ops::Add<Output = T> + core::ops::Mul<Output = T>,
    {
        if self.cols != rhs.rows {
            return Err(MatError::MismatchedDimension);
        }
        let mut out = heapless::Vec::new();
        out.resize_default(self.rows * rhs.cols).unwrap();
        // 三重循环
        for i in 0..self.rows {
            for k in 0..self.cols {
                for j in 0..rhs.cols {
                    let idx = i * rhs.cols + j;
                    out[idx] = out[idx] + self.at(i, k) * rhs.at(k, j);
                }
            }
        }
        Ok(out)
    }

    #[inline(always)]
    fn at(&self, r: usize, c: usize) -> T {
        self.data[r * self.cols + c]
    }
}

关键点:

  • 错误类型 MatError 只占 1 字节;
  • 整个计算链通过 Result 传递,无 Box<dyn Error>
  • 因为 heapless::Vec 在栈上,所以 无系统 allocator 依赖,适合裸机。

4. 零成本实战 2:Option 与 SIMD 的无缝配合

4.1 背景:用 SIMD 处理 1024 个 Option<f32>

如果 Option<f32> 是朴素布局,我们需要两个数组:一个 f32,一个 bool,SIMD 没法一次加载。
但我们可以手动做 niche optimization:

#[repr(transparent)]
pub struct NicheF32(u32);

impl NicheF32 {
    const NONE: u32 = 0xFF_FF_FF_FF; // NaN
    #[inline]
    pub fn new(v: f32) -> Option<Self> {
        let bits = v.to_bits();
        if bits == Self::NONE { None } else { Some(Self(bits)) }
    }
    #[inline]
    pub fn get(&self) -> Option<f32> {
        if self.0 == Self::NONE { None } else { Some(f32::from_bits(self.0)) }
    }
}

4.2 批量转换:一次 8 个

use core::arch::x86_64::*;

#[target_feature(enable = "avx2")]
pub unsafe fn batch_sum(values: &[NicheF32]) -> f32 {
    let mut acc = _mm256_setzero_ps();
    let none = _mm256_set1_ps(f32::from_bits(NicheF32::NONE));
    let mut ptr = values.as_ptr();
    let end = ptr.add(values.len());
    while ptr < end {
        let raw = _mm256_loadu_ps(ptr as *const f32);
        let mask = _mm256_cmp_ps(raw, none, _CMP_NEQ_UQ);
        let masked = _mm256_and_ps(raw, _mm256_castsi256_ps(_mm256_castps_si256(mask)));
        acc = _mm256_add_ps(acc, masked);
        ptr = ptr.add(8);
    }
    // horizontal sum
    let low = _mm256_castps256_ps128(acc);
    let high = _mm256_extractf128_ps(acc, 1);
    let sum128 = _mm_add_ps(low, high);
    let sum64 = _mm_hadd_ps(sum128, sum128);
    let sum32 = _mm_hadd_ps(sum64, sum64);
    _mm_cvtss_f32(sum32)
}

内存视角

  • 输入 &[NicheF32]&[u32]/&[f32] 同布局;
  • 没有额外 tag,SIMD 可以一次加载 8 个元素;
  • None 用特殊 NaN 表示,不牺牲性能。

5. 零成本实战 3:把 Result 嵌入 FFI

5.1 C ABI 兼容的错误传播

某些嵌入式芯片只支持 C ABI。我们设计一个返回 Result<u8, ErrorCode> 的函数,如何在 不引入 panic 的情况下暴露给 C?

#[repr(u8)]
#[derive(Copy, Clone)]
pub enum ErrorCode {
    Ok = 0,
    BadLength,
    BadChecksum,
}

#[no_mangle]
pub extern "C" fn parse_packet(ptr: *const u8, len: usize) -> u8 {
    let slice = unsafe { core::slice::from_raw_parts(ptr, len) };
    match parse_inner(slice) {
        Ok(_) => ErrorCode::Ok as u8,
        Err(e) => e as u8,
    }
}

fn parse_inner(data: &[u8]) -> Result<(), ErrorCode> {
    if data.len() < 4 { return Err(ErrorCode::BadLength); }
    let sum: u8 = data.iter().fold(0, |a, &b| a.wrapping_add(b));
    if sum != 0 { return Err(ErrorCode::BadChecksum); }
    Ok(())
}
  • ErrorCode 只占 1 字节;
  • 返回给 C 的也是 1 字节整数,无 ABI 边界问题;
  • 内部依旧享受 Result 的零成本抽象。

6. 当零成本还不够:手工 enum 的时机

标准库已经做到极致,但极端场景仍需手动控制:

  1. 内存布局敏感
    比如 MMIO 寄存器描述,要求 bit-level 精确。
    这时用 #[repr(C, u32)] 自己写 enum,再配合 transmute

  2. 极端优化
    如果 Result<T, E>E 太大,且你确定错误极少,可改为:

    #[repr(transparent)]
    struct CompressedResult<T, E>(T); // 成功时直接 T
    // 失败时通过全局静态或 TLS 存放 E
    
  3. 跨语言共享
    C++ 20 的 std::expected 与 Rust 的 Result 并非一一对应,需自定义布局。


7. 性能基准:Option/Result vs 手动检查

我们用 criterion 测试 1 亿次 u8 解析:

fn bench_std(b: &mut Bencher) {
    b.iter(|| black_box("123").parse::<u8>().unwrap())
}

fn bench_manual(b: &mut Bencher) {
    b.iter(|| {
        let s = black_box("123");
        let mut v: u8 = 0;
        for &c in s.as_bytes() {
            if c < b'0' || c > b'9' { panic!() }
            v = v * 10 + (c - b'0');
        }
        v
    })
}

结果(AMD 5950x, Rust 1.76, opt-level=3):

函数时间/ns
parse::<u8>2.4
手写2.3

差异 < 5%,在测量误差内。
再次证明:标准库的零成本承诺是可信的


8. 零成本之外的收益:API 语义与可组合性

零成本不等于零思考。
Option/Result 的最大价值在于语义提升

  • 类型系统强迫调用者处理失败路径,消除“忘记检查”类 bug;
  • ? 语法糖让错误传播变成线性流程,取代深层嵌套;
  • Iterator<Item = Result<T, E>> 可与 collect::<Result<Vec<_>, _>>() 优雅收集。

9. 总结与展望

维度Option/Result手写检查 / C 异常
内存1 byte ~ T 本身额外哨兵 / 堆分配
分支预测与手写一致(inline)间接调用 / 栈展开
语义类型系统级文档级
可组合性高(?、迭代器、闭包)
FFI 兼容性通过 #[repr] 可零成本转换需手动转换

未来方向:

  • #[optimize(none)] 调试构建下,仍希望保留 Result 的调试信息,但不损失 release 性能;
  • const Result<T, E>:在 const 上下文中传播错误;
  • 语言级 try trait 持续演进,允许自定义 ? 的行为。

当你下一次写 fn read() -> Result<Vec<u8>, io::Error> 时,请自信地告诉自己:
“我写的不仅是一段错误处理逻辑,更是一份对零成本抽象哲学的签名。” 🦀
在这里插入图片描述

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐