我正在实施康威的生活游戏来自学 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,
}
Run Code Online (Sandbox Code Playgroud)
给我带来麻烦的功能:
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] {
/* ... */
}
}
Run Code Online (Sandbox Code Playgroud)
该特征RWSelector具有用于读取和写入单元元组 ( CellRW) 的静态函数。它是为两个零大小的类型的实现L和R和主要是为了避免不必编写用于不同的访问模式的不同方法的一种方式。
该iter_mut()方法返回一个BoardIter结构体,它是单元格向量的可变切片迭代器的包装器,因此具有&mut CellRWasItem类型。它还知道当前BoardPos(x 和 y 坐标、偏移量)。
我以为我会遍历所有单元格元组,跟踪坐标,计算每个(读取)单元格的活动邻居数(我需要知道坐标/偏移量),计算下一代的单元格状态和写入元组的另一半。
当然,最后编译器向我展示了我设计中的致命缺陷,因为我self.board在iter_mut()方法中可变地借用,然后尝试再次不变地借用它以获取读取单元的所有邻居。
到目前为止,我还没有能够为这个问题提出一个好的解决方案。我确实通过使所有引用不可变,然后使用 anUnsafeCell将写入单元的不可变引用转换为可变引用来使其工作。然后我通过UnsafeCell. 然而,这并不是一个合理的设计,我怀疑我在尝试并行化时可能会遇到问题。
有没有办法实现我在安全/惯用的 Rust 中提出的数据布局,或者这实际上是一种您实际上必须使用技巧来规避 Rust 的别名/借用限制的情况?
另外,作为一个更广泛的问题,对于需要您绕过 Rust 借用限制的问题,是否存在可识别的模式?
什么时候需要绕过 Rust 的借用检查器?
在以下情况下需要它:
作为一个具体案例,编译器无法判断这是安全的:
let mut array = [1, 2];
let a = &mut array[0];
let b = &mut array[1];
Run Code Online (Sandbox Code Playgroud)
编译器不知道IndexMut在编译的这一点上对切片的实现做了什么(这是一个深思熟虑的设计选择)。就它所知,数组总是返回完全相同的引用,而不管索引参数如何。我们可以说这段代码是安全的,但编译器不允许。
你可以用一种对编译器明显安全的方式重写它:
let mut array = [1, 2];
let (a, b) = array.split_at_mut(1);
let a = &mut a[0];
let b = &mut b[0];
Run Code Online (Sandbox Code Playgroud)
这是怎么做的?split_at_mut执行运行时检查以确保它实际上是安全的:
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))
}
}
Run Code Online (Sandbox Code Playgroud)
对于那些借检查不举例尚未先进,因为它可以,看什么都是非词汇的寿命?.
我
self.board在iter_mut()方法中可变地借用,然后尝试再次不变地借用它以获取读取单元格的所有邻居。
如果你知道引用不重叠,那么你可以选择使用不安全的代码来表达它。但是,这意味着您也选择承担维护 Rust 的所有不变量并避免未定义行为的责任。
好消息是,每个 C 和 C++ 程序员都必须(或至少应该)为他们编写的每一行代码承担这种沉重的负担。至少在 Rust 中,你可以让编译器处理 99% 的情况。
在许多情况下,有喜欢的工具Cell,并RefCell允许内部突变。在其他情况下,您可以重写算法以利用作为Copy类型的值。在其他情况下,您可以在更短的时间内对切片使用索引。在其他情况下,您可以使用多阶段算法。
如果您确实需要求助于unsafe代码,那么请尽量将其隐藏在一个小区域内并公开安全接口。
最重要的是,之前(多次)询问了许多常见问题:
| 归档时间: |
|
| 查看次数: |
731 次 |
| 最近记录: |