设计样条函子的更可靠方案:改进BSpline不安全实现
问题分析与改进方案
首先展示你提供的原代码:
#[derive(Clone, Debug)] pub struct BSpline { knots: Vec<f32>, control_points: Vec<Vec3>, /// 内部辅助变量,用于优化相近$t$值的重复采样。 last_t_index: usize, order: usize, } impl BSpline { pub fn sample(&self, t: f32) -> Vec3 { debug_assert!(self.control_points.len() >= self.order); let t = f32::clamp(t, self.knots[0], *self.knots.last().unwrap()); let mut t_index = self.last_t_index; while !(t >= self.knots[t_index] && t <= self.knots[t_index + 1]) { t_index = (t_index + 1) % self.knots.len(); } // TODO(low): 寻找比该hack更优的实现方式。 let ptr = &self.last_t_index as *const usize as *mut usize; unsafe { ptr.write(usize::min(t_index, self.order - 1)); }; // 创建缓存以计算中间求值的金字塔结构。 let order = self.order; let degree = order - 1; let mut evaluation = Vec::new(); for i in 0..order { evaluation.push(self.control_points[t_index - degree + i]); } for l in 0..degree { for j in ((l + 1)..=degree).rev() { let alpha = (t - self.knots[t_index - degree + j]) / (self.knots[t_index - l + j] - self.knots[t_index - degree + j]); evaluation[j] = (1.0 - alpha) * evaluation[j - 1] + alpha * evaluation[j]; } } evaluation[degree] } }
这段代码通过unsafe强制转换指针修改不可变引用内的last_t_index,违反了Rust的可变/不可变引用规则,即使单线程下能运行,也存在未定义行为风险。结合你的约束(避免FnMut克隆、拒绝锁/原子开销、UnsafeCell不可Clone),以下是几个改进方案:
方案1:自定义Clone的RefCell包装索引
将last_t_index用RefCell包装,手动实现Clone trait确保克隆实例的索引重置为初始值,完全规避unsafe代码。
use std::cell::RefCell; #[derive(Debug)] pub struct BSpline { knots: Vec<f32>, control_points: Vec<Vec3>, last_t_index: RefCell<usize>, order: usize, } impl Clone for BSpline { fn clone(&self) -> Self { Self { knots: self.knots.clone(), control_points: self.control_points.clone(), // 克隆时重置索引,避免继承原实例的缓存值 last_t_index: RefCell::new(0), order: self.order, } } } impl BSpline { pub fn sample(&self, t: f32) -> Vec3 { debug_assert!(self.control_points.len() >= self.order); let t = f32::clamp(t, self.knots[0], *self.knots.last().unwrap()); let mut t_index = *self.last_t_index.borrow(); while !(t >= self.knots[t_index] && t <= self.knots[t_index + 1]) { t_index = (t_index + 1) % self.knots.len(); } // 安全更新缓存 *self.last_t_index.borrow_mut() = usize::min(t_index, self.order - 1); // 后续求值逻辑不变 let order = self.order; let degree = order - 1; let mut evaluation = Vec::new(); for i in 0..order { evaluation.push(self.control_points[t_index - degree + i]); } for l in 0..degree { for j in ((l + 1)..=degree).rev() { let alpha = (t - self.knots[t_index - degree + j]) / (self.knots[t_index - l + j] - self.knots[t_index - degree + j]); evaluation[j] = (1.0 - alpha) * evaluation[j - 1] + alpha * evaluation[j]; } } evaluation[degree] } }
优点:
- 完全符合Rust安全规则,无unsafe代码
- 结构简单,无需额外ID或线程存储
sample仍接收&self,适配原有的Fn约束,避免冗余克隆
缺点:
RefCell的运行时借用检查有极轻微开销,但单线程下无panic风险
方案2:线程局部存储缓存索引
将last_t_index移出结构体,用线程局部哈希表存储每个BSpline实例的最近索引,通过唯一ID关联。
use std::cell::RefCell; use std::collections::HashMap; thread_local! { static LAST_T_INDEX_CACHE: RefCell<HashMap<usize, usize>> = RefCell::new(HashMap::new()); } #[derive(Clone, Debug)] pub struct BSpline { knots: Vec<f32>, control_points: Vec<Vec3>, order: usize, id: usize, } static NEXT_ID: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0); impl BSpline { pub fn new(knots: Vec<f32>, control_points: Vec<Vec3>, order: usize) -> Self { let id = NEXT_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed); Self { knots, control_points, order, id } } pub fn sample(&self, t: f32) -> Vec3 { debug_assert!(self.control_points.len() >= self.order); let t = f32::clamp(t, self.knots[0], *self.knots.last().unwrap()); // 从线程缓存获取上次索引,默认用0 let mut t_index = LAST_T_INDEX_CACHE.with(|cache| { cache.borrow().get(&self.id).copied().unwrap_or(0) }); while !(t >= self.knots[t_index] && t <= self.knots[t_index + 1]) { t_index = (t_index + 1) % self.knots.len(); } // 更新线程缓存 LAST_T_INDEX_CACHE.with(|cache| { cache.borrow_mut().insert(self.id, usize::min(t_index, self.order - 1)); }); // 后续求值逻辑不变 let order = self.order; let degree = order - 1; let mut evaluation = Vec::new(); for i in 0..order { evaluation.push(self.control_points[t_index - degree + i]); } for l in 0..degree { for j in ((l + 1)..=degree).rev() { let alpha = (t - self.knots[t_index - degree + j]) / (self.knots[t_index - l + j] - self.knots[t_index - degree + j]); evaluation[j] = (1.0 - alpha) * evaluation[j - 1] + alpha * evaluation[j]; } } evaluation[degree] } } impl Clone for BSpline { fn clone(&self) -> Self { let id = NEXT_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed); Self { knots: self.knots.clone(), control_points: self.control_points.clone(), order: self.order, id, } } }
优点:
- 无unsafe代码,完全安全
- 单线程下无额外运行时开销,原子操作仅在实例创建/克隆时触发
- 不影响原有的Fn约束
缺点:
- 需要维护实例唯一ID,大量实例时哈希表有轻微查找开销
方案3:手动实现Clone的UnsafeCell方案
使用UnsafeCell明确标记内部可变性,手动实现Clone和安全的Send/Sync(单线程下合法),比原hack更规范。
use std::cell::UnsafeCell; #[derive(Debug)] pub struct BSpline { knots: Vec<f32>, control_points: Vec<Vec3>, last_t_index: UnsafeCell<usize>, order: usize, } impl Clone for BSpline { fn clone(&self) -> Self { Self { knots: self.knots.clone(), control_points: self.control_points.clone(), // 克隆时初始化索引为0 last_t_index: UnsafeCell::new(0), order: self.order, } } } // 单线程下BSpline可安全Send/Sync unsafe impl Send for BSpline {} unsafe impl Sync for BSpline {} impl BSpline { pub fn sample(&self, t: f32) -> Vec3 { debug_assert!(self.control_points.len() >= self.order); let t = f32::clamp(t, self.knots[0], *self.knots.last().unwrap()); // 单线程下安全读取UnsafeCell let mut t_index = unsafe { *self.last_t_index.get() }; while !(t >= self.knots[t_index] && t <= self.knots[t_index + 1]) { t_index = (t_index + 1) % self.knots.len(); } // 单线程下安全写入UnsafeCell unsafe { *self.last_t_index.get() = usize::min(t_index, self.order - 1); }; // 后续求值逻辑不变 let order = self.order; let degree = order - 1; let mut evaluation = Vec::new(); for i in 0..order { evaluation.push(self.control_points[t_index - degree + i]); } for l in 0..degree { for j in ((l + 1)..=degree).rev() { let alpha = (t - self.knots[t_index - degree + j]) / (self.knots[t_index - l + j] - self.knots[t_index - degree + j]); evaluation[j] = (1.0 - alpha) * evaluation[j - 1] + alpha * evaluation[j]; } } evaluation[degree] } }
优点:
- 性能几乎和原代码一致,无额外开销
- 用
UnsafeCell明确标记可变性,符合Rust内存模型,比原hack更安全 sample仍接收&self,适配原约束
缺点:
- 仍需少量unsafe代码,且需手动实现Clone和Send/Sync
内容的提问来源于stack exchange,提问作者Makogan
相关产品推荐
相关产品推荐

