Skip to content

Rust 性能与 unsafe 边界:先测量,再证明不变量 ​

前置:所有权、线程项目。继续使用同一个 concurrency-lab,不安装外部基准库。性能实验和内存安全证明是两件事:测量不会证明安全,安全也不会保证更快。

等价语义与测量 ​

为了得到词数,构造 Vec<String> 会为词分配并复制,直接 count 不需要保存词。先断言同一输入结果相同,再用 release 构建、black_box 和重复执行降低优化掉实验的风险。完整程序:

rs
use std::{hint::black_box, time::Instant};
fn allocated(text: &str) -> usize {
    text.split_whitespace().map(str::to_owned).collect::<Vec<_>>().len()
}
fn borrowed(text: &str) -> usize { text.split_whitespace().count() }
fn main() {
    let text = "rust text word ".repeat(1000);
    assert_eq!(allocated(&text), borrowed(&text));
    for (name, f) in [("allocated", allocated as fn(&str)->usize), ("borrowed", borrowed)] {
        let start = Instant::now();
        for _ in 0..1000 { black_box(f(black_box(&text))); }
        println!("{name}: {:?}", start.elapsed());
    }
}

在项目目录执行 cargo run --release --offline --example performance,输出 allocated/borrowed 两个耗时;每项1000次、同一输入。不要固定某台机器的阈值,也不要把一次实验当成所有负载的倍数。它不是统计基准框架,没有控制CPU频率、隔离其他进程或生成置信区间。

此处可确定 allocated 的源代码请求为每个词构造 String;真实分配次数与机器码仍受优化器影响。借用切片避免复制,但保留大输入中的小切片会延长整个输入生命;需要长期只保存一小部分时,复制可能降低存活内存。Vec::with_capacity 减少扩容,容量过大又增加保留;Arc clone 避免复制底层值但有原子计数成本。应先确定是吞吐、延迟、峰值内存还是分配热点,再选择改法。

工具与诊断顺序 ​

先用 cargo check 定位类型/借用错误,再 cargo test --offline 检查测试覆盖的语义,最后 release 比较。cargo rustc --release --offline --example performance -- --emit=asm 可为本页性能示例生成汇编用于具体热点核查;传额外编译器参数必须明确选择单一目标,本工程同时含库和二进制,不能省略目标过滤器。参见 cargo rustc。Linux 若已具备 perf 权限可用 perf 采样;Windows 可用系统CPU采样器。本轮示例与测量均未执行,未运行系统 profiler 或Miri。

稳定版自带 test 可做单测,旧 #[bench]/test::Bencher 教程依赖不稳定能力,不复制到 stable 正式项目。需要严谨基准时选择明确兼容版本的基准工具并固定环境。本项目先用无依赖实验建立“语义不变→重复测量→解释限制”的完整路径。

unsafe 的证明义务 ​

unsafe 允许调用不安全函数、解引用裸指针等操作,它不会关闭借用规则、消除数据竞争、允许越界,或把未定义行为变成可捕获异常。原始指针存在不等于可解引用;必须满足对齐、有效范围、初始化、生命期及别名约束。FFI 还需核对ABI、调用约定、所有权、释放器、字符串终止及跨边界错误;默认 Rust 布局和 ABI 不适合作为 C 契约。

项目 split_checked 仅在 mid≤len 时把同一独占借用拆成两个不重叠范围,返回的生命期来自输入。空切片/ZST 的指针仍须非空对齐,长度0并不许可传 null。指针运算必须留在同一分配内或尾后一位;这里来自合法 Rust 切片并限制 mid,因此成立。两边独占范围不重叠,调用者不能在结果仍被使用时再次访问输入。

实现已完整包含在线程项目 lib.rs,边界测试在行为测试。真实业务直接调用标准库 split_at_mut 或 split_at_mut_checked 更清楚,这个实现是解释证明的实验,不是优化建议。测试至少覆盖 mid=0、len、len+1、空切片、ZST、左右写入。错误边界返回 None,绝不执行越界裸指针操作来“演示崩溃”。

练习 ​

① 为什么 unsafe { ptr.add(mid) } 前必须先检查 mid?② 把引用 transmute 成 'static 能解决借用错误吗?③ borrowed 快是否足以删除所有 String?④ 多线程结果正确是否证明没有数据竞争?

参考解法 ​

① add 的安全前提是地址在同一合法对象或尾后一位;超界运算本身即可违反前提,不必等到解引用。② 不能,标注不能延长分配寿命,返回后可能悬垂。③ 不能,拥有数据对独立存储、跨任务转移等仍必要,测量只针对词数。④ 不能,调度可能没触发竞态;安全Rust约束、正确同步及unsafe证明必须分别成立。

来源 ​

2026-10-17 对照官方中文学习入口、Unsafe Book、from_raw_parts_mut 安全契约、black_box与未定义行为规范。该例教学边界不涵盖自制分配器、无锁容器或内核 no_std。

继续阅读