【问题标题】:When is it necessary to circumvent Rust's borrow checker?什么时候需要绕过 Rust 的借用检查器?
【发布时间】:2018-05-20 22:48:46
【问题描述】:

我正在实施 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)的静态函数。它针对LR 两种零大小类型实现,主要是避免为不同的访问模式编写不同的方法。

iter_mut() 方法返回一个 BoardIter 结构体,它是单元格向量的可变切片迭代器的包装器,因此具有 &amp;mut CellRW 作为 Item 类型。它还知道当前的BoardPos(x 和 y 坐标,偏移量)。

我以为我会遍历所有单元元组,跟踪坐标,计算每个(读取)单元的存活邻居的数量(我需要知道坐标/偏移量),计算单元的状态下一代并写入元组的另一半。

当然,最后,编译器向我展示了我设计中的致命缺陷,因为我在iter_mut() 方法中可变地借用self.board,然后尝试再次不可变地借用它以获取读取的所有邻居细胞。

到目前为止,我还没有为这个问题想出一个好的解决方案。我确实设法通过使所有 引用不可变,然后使用UnsafeCell 将对写入单元格的不可变引用转换为可变引用。 然后我通过UnsafeCell 写入元组写入部分的名义上不可变的引用。 然而,这并没有让我印象深刻的设计,我怀疑我在尝试并行化事情时可能会遇到这个问题。

有没有办法实现我在安全/惯用的 Rust 中提出的数据布局,或者这实际上是您必须使用技巧来规避 Rust 的别名/借用限制的情况?

另外,作为一个更广泛的问题,对于需要您绕过 Rust 的借用限制的问题,是否存在可识别的模式?

【问题讨论】:

  • 使用UnsafeCell——你为什么一直去UnsafeCell而不是例如只是一个Cell?

标签: rust


【解决方案1】:

什么时候需要绕过 Rust 的借用检查器?

在以下情况下需要它:

  • 借用检查器不够先进,无法确保您的使用安全
  • 您不希望(或不能)以不同的模式编写代码

作为一个具体的例子,编译器无法判断这是安全的:

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

编译器不知道IndexMut 在编译时对切片的实现是做什么的(这是经过深思熟虑的设计选择)。据它所知,无论索引参数如何,数组总是返回完全相同的引用。 我们可以看出这段代码是安全的,但编译器不允许这样做。

您可以以对编译器明显安全的方式重写它:

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?

我在iter_mut() 方法中可变地借用self.board,然后尝试再次不可变地借用它以获取读取单元格的所有邻居。

如果你知道引用不重叠,那么你可以选择使用不安全的代码来表达它。然而,这意味着 也选择承担维护 Rust 的所有不变量并避免undefined behavior 的责任。

好消息是,每个 C 和 C++ 程序员都必须(或至少应该)为他们编写的每一行代码承担这个沉重的负担强>。至少在 Rust 中,你可以让编译器处理 99% 的情况。

在许多情况下,有像 CellRefCell 这样的工具来允许内部突变。在其他情况下,您可以重写算法以利用 Copy 类型的值。在其他情况下,您可以在较短的时间内将索引用于切片。在其他情况下,您可以使用多阶段算法。

如果您确实需要使用unsafe 代码,请尽量将其隐藏在一个小区域并暴露安全接口。

首先,许多常见问题已经被问过(很多次):

【讨论】:

  • 感谢您的深入解释。有趣的是,我没想过使用简单的索引循环。我现在无法测试,但这应该是解决我的问题的一种方法。此外,我只浏览了关于细胞的章节,所以我没有注意到它。当我尝试 std::mem::transmute 我的不可变引用时,编译器让我想到了 UnsafeCell。
  • 实现了两个不同的版本,一个带有索引循环,一个带有 std::cell::Cell 使用内部可变性,从一些初步的基准测试看来,使用索引循环明显更快并且使用与我之前的实现相比,内部可变性要慢得多,在之前的实现中,我使用了一个不安全的单元格来改变值,不太清楚为什么会这样,但它很有趣。
  • @Oliver 这对我来说实际上非常违反直觉。 Cell 没有 any 运行时成本,而索引数组应该有一个(微小的)越界检查。您确实使用--releasecargo bench 进行了基准测试,对吗?
  • 是的,我做到了。我也尽了最大的努力使不同的实现尽可能地不同,因此实现 a) 使用两个索引循环 (x,y) 和 unchecked_get_mut() 来访问每个单元,实现 b) 使用单元和一个坐标感知迭代器。当我使用两个带有常规 get_mut() 访问的索引循环时,结果更接近,但循环仍然获胜。我不知道为什么会这样,但我认为一个版本可能允许更多的编译器优化。
猜你喜欢
  • 1970-01-01
  • 2014-09-10
  • 1970-01-01
  • 1970-01-01
  • 2018-01-12
  • 1970-01-01
  • 2012-09-16
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多