我实施Conway的生活游戏,自学Rust。我们的想法是首先实现单线程版本,尽可能地优化它,然后对多线程版本执行相同的操作。
我想实现一种替代数据布局,我认为它可能更适合缓存。这个想法是在一个向量中存储彼此相邻的板上每个点的两个单元的状态,一个单元用于读取当前一代的状态,一个单元用于写入下一代的状态。状态为,交替每个的访问模式 生成计算(可以在编译时确定)。
基本数据结构如下:
#[repr(u8)]
pub enum CellStatus {
DEAD,
ALIVE,
}
/** 2 bytes */
pub struct CellRW(CellStatus, CellStatus);
pub struct TupleBoard {
width: usize,
height: usize,
cells: Vec<CellRW>,
}
/** used to keep track of current pos with iterator e.g. */
pub struct BoardPos {
x_pos: usize,
y_pos: usize,
offset: usize,
}
pub struct BoardEvo {
board: TupleBoard,
}
导致我麻烦的功能:
impl BoardEvo {
fn evolve_step<T: RWSelector>(&mut self) {
for (pos, cell) in self.board.iter_mut() {
//pos: BoardPos, cell: &mut CellRW
let read: &CellStatus = T::read(cell); //chooses the right tuple half for the current evolution step
let write: &mut CellStatus = T::write(cell);
let alive_count = pos.neighbours::<T>(&self.board).iter() //<- can't borrow self.board again!
.filter(|&&status| status == CellStatus::ALIVE)
.count();
*write = CellStatus::evolve(*read, alive_count);
}
}
}
impl BoardPos {
/* ... */
pub fn neighbours<T: RWSelector>(&self, board: &BoardTuple) -> [CellStatus; 8] {
/* ... */
}
}
特征RWSelector
具有用于读取和写入单元格元组(CellRW
)的静态函数。它是针对两种零大小的类型L
和R
实现的,主要是避免为不同的访问模式编写不同方法的方法。
iter_mut()
方法返回一个BoardIter
结构,它是一个包含单元格向量的可变切片迭代器的包装器,因此&mut CellRW
为Item
类型。它也知道当前的BoardPos
(x和y坐标,偏移量)。
我以为我会迭代所有单元格元组,跟踪坐标,计算每个(读取)单元格的活动邻居数量(我需要知道这个的坐标/偏移),计算单元格状态对于下一代并写入相应的另一半元组。
当然,最后,编译器向我展示了我的设计中的致命缺陷,因为我在self.board
方法中可变地借用iter_mut()
然后尝试再次借用它来获取所有读单元的邻居。
到目前为止,我还没能找到解决这个问题的好方法。我确实设法让所有人都努力工作
引用不可变,然后使用UnsafeCell
将写入单元格的不可变引用转换为可变引用。
然后我通过UnsafeCell
写入对元组写作部分的名义上不可变的引用。
然而,这并没有让我觉得这是一个合理的设计,我怀疑在尝试并行化时,我可能会遇到这个问题。
有没有办法实现我在安全/惯用Rust中提出的数据布局,还是实际上你需要使用技巧来规避Rust的别名/借用限制呢?
另外,作为一个更广泛的问题,是否有一个可识别的问题模式需要你规避Rust的借款限制?
答案 0 :(得分:4)
什么时候必须绕过Rust的借阅检查器?
在以下情况下需要:
作为具体案例,编译器无法确定这是安全的:
let mut array = [1, 2];
let a = &mut array[0];
let b = &mut array[1];
编译器不知道切片的IndexMut
的实现在此编译时是什么(这是一个深思熟虑的设计选择)。尽管如此,无论index参数如何,数组总是返回完全相同的引用。 我们可以判断此代码是安全的,但编译器不允许这样做。
您可以用对编译器显然安全的方式重写它:
let mut array = [1, 2];
let (a, b) = array.split_at_mut(1);
let a = &mut a[0];
let b = &mut b[0];
这是怎么做到的? split_at_mut
performs a runtime check确保 安全
fn split_at_mut(&mut self, mid: usize) -> (&mut [T], &mut [T]) {
let len = self.len();
let ptr = self.as_mut_ptr();
unsafe {
assert!(mid <= len);
(from_raw_parts_mut(ptr, mid),
from_raw_parts_mut(ptr.offset(mid as isize), len - mid))
}
}
有关借阅检查器尚未的示例,请参阅What are non-lexical lifetimes?。
我在
self.board
方法中可变地借用iter_mut()
,然后尝试再次借用它来获取读取单元格的所有邻居。
如果您知道引用不重叠,那么您可以选择使用不安全的代码来表达它。但是,这意味着你也选择承担维护所有Rust不变量并避免undefined behavior的责任。
好消息是,每个C和C ++程序员必须(或者至少应该)拥有他们编写的每一行代码> STRONG>。至少在Rust中,你可以让编译器处理99%的情况。
在许多情况下,有Cell
和RefCell
等工具可以进行内部变异。在其他情况下,您可以重写算法以利用Copy
类型的值。在其他情况下,您可以在较短的时间段内将索引用于切片。在其他情况下,您可以使用多阶段算法。
如果您确实需要使用unsafe
代码,请尽量将其隐藏在一个小区域中并公开安全接口。
最重要的是,之前曾多次提出许多常见问题: