如何安全地将字符串及其切片移入Rust闭包?
Rust闭包中存储字符串切片的无额外分配方案探讨
我希望创建一个闭包,在其中存储原字符串的切片集合(Vec<&str>),同时避免额外的堆内存分配。
初始实现(安全但有额外分配)
我最先实现的方案可以正常工作,但会为每个拆分出的单词创建新String,带来额外堆内存分配:
fn main() { let sentence = "Foo to the bar".to_string(); let my_closure = get_closure(sentence); my_closure(); } fn get_closure(sentence: String) -> impl Fn() { // 为每个单词分配新的String let words: Vec<String> = sentence.split_whitespace().map(ToString::to_string).collect(); move || { for word in &words { word.as_bytes().iter().map(|n| *n as usize).sum::<usize>(); } } }
Unsafe尝试(无额外分配但存疑)
为了减少内存开销,我用unsafe和transmute强制延长切片生命周期,同时将原字符串移入闭包,只存储切片的Vec:
fn main() { let sentence = "Foo to the bar".to_string(); let my_closure = get_closure(sentence); my_closure(); } fn get_closure(sentence: String) -> impl Fn() { let words: Vec<&'static str> = unsafe { std::mem::transmute(sentence.split_whitespace().collect::<Vec<&str>>()) }; move || { let _mover = &sentence; for word in &words { word.as_bytes().iter().map(|n| *n as usize).sum::<usize>(); } } }
这个方案能运行,但我有两个疑问:
- 有没有更简洁的无额外堆分配方案(最好不使用unsafe)?
- 上述unsafe方案是否安全?
背景补充:此场景属于性能热点,我希望避免每次调用闭包都执行拆分操作。目前可行的安全方案是将拆分结果克隆成Vec<String>存入闭包,但存在额外分配。
基准测试对比
我编写了基准测试对比两种方案的性能:
use criterion::{black_box, criterion_group, criterion_main, Criterion}; fn get_closure_unsafe(sentence: String) -> impl Fn(usize) { let words: Vec<&'static str> = unsafe { std::mem::transmute(sentence.split_whitespace().collect::<Vec<&str>>()) }; move |_: usize| { let _mover = &sentence; for word in &words { word.as_bytes().iter().map(|n| *n as usize).sum::<usize>(); } } } fn get_closure_safe(sentence: String) -> impl Fn(usize) { let words: Vec<String> = sentence .split_whitespace() .map(ToString::to_string) .collect(); move |_: usize| { for word in &words { word.as_bytes().iter().map(|n| *n as usize).sum::<usize>(); } } } fn bench_fibs(c: &mut Criterion) { let mut group = c.benchmark_group("Str split"); let sentence = "Foo to the bar".to_string(); let safe_closure = get_closure_safe(sentence.clone()); group.bench_function("safe", |b| b.iter(|| safe_closure(black_box(1)))); let unsafe_closure = get_closure_unsafe(sentence); group.bench_function("unsafe", |b| b.iter(|| unsafe_closure(black_box(1)))); group.finish(); } criterion_group!(comparison, bench_fibs); criterion_main!(comparison);
测试结果
Str split/safe time: [214.28 ps 214.48 ps 214.73 ps] change: [-1.4155% -1.0571% -0.7363%] (p = 0.00 < 0.05) Change within noise threshold. Found 14 outliers among 100 measurements (14.00%) 4 (4.00%) high mild 10 (10.00%) high severe Str split/unsafe time: [214.12 ps 214.35 ps 214.82 ps] change: [-0.5763% -0.4318% -0.2713%] (p = 0.00 < 0.05) Change within noise threshold. Found 12 outliers among 100 measurements (12.00%) 7 (7.00%) high mild 5 (5.00%) high severe
疑问解答
1. 无unsafe的无额外分配方案
完全存在,只需将原字符串和切片集合同时存入闭包,Rust编译器会自动推导生命周期,保证切片始终指向有效内存:
fn main() { let sentence = "Foo to the bar".to_string(); let my_closure = get_closure(sentence); my_closure(); } fn get_closure(sentence: String) -> impl Fn() { let words: Vec<&str> = sentence.split_whitespace().collect(); move || { // sentence被移入闭包,words的切片始终指向有效内存 for word in &words { word.as_bytes().iter().map(|n| *n as usize).sum::<usize>(); } } }
这个方案完全安全,Vec<&str>仅存储指针和长度,不复制字符串内容,同时原字符串被移入闭包,确保切片生命周期与闭包一致,无任何额外堆分配。
2. 你的unsafe方案是否安全?
不安全,存在严重未定义行为风险:
transmute将绑定到sentence生命周期的Vec<&str>强制转为Vec<&'static str>,欺骗编译器认为切片是静态生命周期,但实际上切片的有效性完全依赖闭包中的sentence。- 虽然你加入
let _mover = &sentence;试图保留sentence,但这无法改变切片的实际生命周期。一旦编译器优化掉该引用,或者后续代码将words单独移出闭包,就会产生悬垂指针,访问已释放内存,触发不可预测的错误。 - 从类型系统角度,
&str和&'static str的内存布局虽然相同,但生命周期是Rust安全保障的核心,强制修改会绕过所有安全检查,导致未定义行为。
基准测试结果说明
本次测试仅测量了闭包内部的计算逻辑,未包含字符串拆分和内存分配的开销。若加入拆分步骤,无分配方案(包括上述安全方案和你的unsafe方案)会比Vec<String>方案更快,因为避免了堆内存分配和字符串复制。
内容的提问来源于stack exchange,提问作者airblast
相关产品推荐
相关产品推荐

