简体   繁体   English

什么时候有必要规避Rust的借阅检查器?

[英]When is it necessary to circumvent Rust's borrow checker?

I'm implementing Conway's game of life to teach myself Rust. 我正在实施Conway的生活游戏以自学Rust。 The idea is to implement a single-threaded version first, optimize it as much as possible, then do the same for a multi-threaded version. 这个想法是首先实现一个单线程版本,尽可能地对其进行优化,然后对一个多线程版本执行相同的操作。

I wanted to implement an alternative data layout which I thought might be more cache-friendly. 我想实现另一种数据布局,我认为这可能对缓存更友好。 The idea is to store the status of two cells for each point on a board next to each other in memory in a vector, one cell for reading the current generation's status from and one for writing the next generation's status to, alternating the access pattern for each generation's computation (which can be determined at compile time). 这个想法是将板上两个点的每个单元的状态彼此相邻地存储在向量中的内存中,一个单元用于从中读取当前状态,而另一个单元用于写入下一代状态,以交替访问模式。每一代的计算(可以在编译时确定)。

The basic data structures are as follows: 基本数据结构如下:

#[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,
}

The function that is causing me troubles: 引起我麻烦的功能:

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] {
        /* ... */
    }
}

The trait RWSelector has static functions for reading from and writing to a cell tuple ( CellRW ). 特征RWSelector具有用于读取和写入单元元组( CellRW )的静态功能。 It is implemented for two zero-sized types L and R and is mainly a way to avoid having to write different methods for the different access patterns. 它是针对两个零大小的类型LR ,主要是一种避免必须为不同的访问模式编写不同方法的方法。

The iter_mut() method returns a BoardIter struct which is a wrapper around a mutable slice iterator for the cells vector and thus has &mut CellRW as Item type. iter_mut()方法返回一个BoardIter结构,该结构是用于单元格向量的可变切片迭代器的包装,因此具有&mut CellRW作为Item类型。 It is also aware of the current BoardPos (x and y coordinates, offset). 它还知道当前的BoardPos (x和y坐标,偏移)。

I thought I'd iterate over all cell tuples, keep track of the coordinates, count the number of alive neighbours (I need to know coordinates/offsets for this) for each (read) cell, compute the cell status for the next generation and write to the respective another half of the tuple. 我以为我要遍历所有单元元组,跟踪坐标,计算每个(读取)单元的存活邻居数(我需要知道这个的坐标/偏移量),计算下一代单元的状态,并分别写入元组的另一半。

Of course, in the end, the compiler showed me the fatal flaw in my design, as I borrow self.board mutably in the iter_mut() method and then try to borrow it again immutably to get all the neighbours of the read cell. 当然,在结束时,编译器给我致命的缺陷在我的设计,我借self.board在性情不定地iter_mut()方法,然后尝试再次借它一成不变得到读取单元的所有邻居。

I have not been able to come up with a good solution for this problem so far. 到目前为止,我还不能为这个问题提供一个好的解决方案。 I did manage to get it working by making all references immutable and then using an UnsafeCell to turn the immutable reference to the write cell into a mutable one. 我确实设法通过使所有引用不变,然后使用UnsafeCell将对写单元的不变引用转换为可变引用来使其工作。 I then write to the nominally immutable reference to the writing part of the tuple through the UnsafeCell . 然后,我通过UnsafeCell向元组的书写部分写入名义上不变的引用。 However, that doesn't strike me as a sound design and I suspect I might run into issues with this when attempting to parallelize things. 但是,这并不是一个好的声音设计,我怀疑在尝试并行处理时可能会遇到这个问题。

Is there a way to implement the data layout I proposed in safe/idiomatic Rust or is this actually a case where you actually have to use tricks to circumvent Rust's aliasing/borrow restrictions? 有没有一种方法可以实现我在安全/惯用Rust中提出的数据布局,或者这实际上是您实际上必须使用技巧来规避Rust的别名/借用限制的情况吗?

Also, as a broader question, is there a recognizable pattern for problems which require you to circumvent Rust's borrow restrictions? 另外,作为一个更广泛的问题,是否存在可识别的问题模式,这些问题要求您规避Rust的借贷限制?

When is it necessary to circumvent Rust's borrow checker? 什么时候有必要规避Rust的借阅检查器?

It is needed when: 在以下情况下需要它:

  • the borrow checker is not advanced enough to see that your usage is safe 借阅检查器的功能不够先进,无法确保您的使用安全
  • you do not wish to (or cannot) write the code in a different pattern 您不希望(或不能)以其他方式编写代码

As a concrete case, the compiler cannot tell that this is safe: 作为一个具体情况,编译器无法告诉您这是安全的:

let mut array = [1, 2];
let a = &mut array[0];
let b = &mut array[1];

The compiler doesn't know what the implementation of IndexMut for a slice does at this point of compilation (this is a deliberate design choice). 编译器在编译时不知道切片的IndexMut实现会做什么(这是有意的设计选择)。 For all it knows, arrays always return the exact same reference, regardless of the index argument. 就其所知,无论索引参数如何,数组始终返回完全相同的引用。 We can tell that this code is safe, but the compiler disallows it. 我们可以说这段代码是安全的,但是编译器不允许这样做。

You can rewrite this in a way that is obviously safe to the compiler: 您可以用对编译器显然安全的方式重写此代码:

let mut array = [1, 2];
let (a, b) = array.split_at_mut(1);
let a = &mut a[0];
let b = &mut b[0];

How is this done? 怎么做? split_at_mut performs a runtime check to ensure that it actually is safe: 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))
    }
}

For an example where the borrow checker is not yet as advanced as it can be, see What are non-lexical lifetimes? 有关借位检查器尚未达到其高级要求的示例,请参阅什么是非词法生存期? .

I borrow self.board mutably in the iter_mut() method and then try to borrow it again immutably to get all the neighbours of the read cell. 我借self.board性情不定地在iter_mut()方法,然后尝试再次借它一成不变得到读取单元的所有邻居。

If you know that the references don't overlap, then you can choose to use unsafe code to express it. 如果您知道引用不重叠,则可以选择使用不安全的代码来表达它。 However, this means you are also choosing to take on the responsibility of upholding all of Rust's invariants and avoiding undefined behavior . 但是,这意味着还选择承担维护Rust的所有不变式和避免未定义行为的责任。

The good news is that this heavy burden is what every C and C++ programmer has to (or at least should ) have on their shoulders for every single line of code they write . 好消息是,每位C和C ++程序员对于编写的每一行代码都必须(或至少应该 )肩负起沉重的负担。 At least in Rust, you can let the compiler deal with 99% of the cases. 至少在Rust中,您可以让编译器处理99%的情况。

In many cases, there's tools like Cell and RefCell to allow for interior mutation. 在许多情况下,可以使用诸如CellRefCell之类的工具进行内部突变。 In other cases, you can rewrite your algorithm to take advantage of a value being a Copy type. 在其他情况下,您可以重写算法以利用作为Copy类型的值。 In other cases you can use an index into a slice for a shorter period. 在其他情况下,您可以在较短时间内使用切片索引。 In other cases you can have a multi-phase algorithm. 在其他情况下,您可以使用多阶段算法。

If you do need to resort to unsafe code, then try your best to hide it in a small area and expose safe interfaces. 如果确实需要使用unsafe代码,请尽力将其隐藏在较小的区域中并暴露安全的接口。

Above all, many common problems have been asked about (many times) before: 最重要的是,以前(很多次)曾问过许多常见问题:

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM