在 Copy 类型中使用 std::p tr::write_volatile 实现内部可变性的安全性(即没有 UnsafeCell)

Safety of using std::ptr::write_volatile for interior mutability in a Copy type (i.e. without UnsafeCell)

提问人:Ricardo Machado 提问时间:10/29/2023 最后编辑:John KugelmanRicardo Machado 更新时间:10/29/2023 访问量:78

问:

我正在尝试在值类型中实现内部可变性(用于缓存目的)。Copy

问题在于,据我所知,没有一种类型可用于内部可变性(例如 和相关类型,原子类型)允许该特征。顺便说一句,这是稳定的 RustUnsafeCellCopy

我的问题是:在这种情况下,使用 std::p tr::write_volatile 和 std:p tr::read_volatile 实现内部可变性的安全性如何?

具体来说,我有这段代码,我想知道这里面是否有任何陷阱或未定义的行为:

use std::fmt::{Debug, Display, Formatter};

#[derive(Copy, Clone)]
pub struct CopyCell<T: Copy> {
    value: T,
}

impl<T: Copy> CopyCell<T> {
    pub fn new(value: T) -> Self {
        Self { value }
    }

    pub fn get(&self) -> T {
        unsafe { std::ptr::read_volatile(&self.value) }
    }

    pub fn set(&self, value: T) {
        let ptr = &self.value as *const T;
        let ptr = ptr as *mut T;
        unsafe { std::ptr::write_volatile(ptr, value) }
    }
}

impl<T: Default + Copy> Default for CopyCell<T> {
    fn default() -> Self {
        CopyCell { value: T::default() }
    }
}

impl<T: Display + Copy> Display for CopyCell<T> {
    fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
        let value = self.get();
        write!(f, "{value}")
    }
}

impl<T: Debug + Copy> Debug for CopyCell<T> {
    fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
        let value = self.get();
        write!(f, "{value:?}")
    }
}

我的目标是使用它在值类型中实现本地缓存(例如,一种记忆化)。我特别想要语义(即代替),因为它们对我想要实现的目标有更好的人体工程学。CopyClone

下面是一个示例用法:

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn simple_test() {
        let a = CopyCell::default();
        let b = CopyCell::new(42);

        assert_eq!(a.get(), 0);
        assert_eq!(b.get(), 42);

        a.set(123);
        b.set(b.get() * 10);
        assert_eq!(a.get(), 123);
        assert_eq!(b.get(), 420);

        let a1 = a;
        let b1 = b;

        a.set(0);
        b.set(0);

        assert_eq!(a1.get(), 123);
        assert_eq!(b1.get(), 420);
    }

    #[test]
    fn cached_compute() {
        let a = CachedCompute::new(10);
        assert_eq!(a.compute(), 100);
        assert_eq!(a.compute(), 100);

        let b = a;
        assert_eq!(b.compute(), 100);
    }

    #[derive(Copy, Clone)]
    pub struct CachedCompute {
        source: i32,
        result: CopyCell<Option<i32>>,
    }

    impl CachedCompute {
        pub fn new(source: i32) -> Self {
            Self {
                source,
                result: Default::default(),
            }
        }

        pub fn compute(&self) -> i32 {
            if let Some(value) = self.result.get() {
                value
            } else {
                let result = self.source * self.source; // assume this is expensive
                self.result.set(Some(result));
                result
            }
        }
    }
}

上面的代码适用于第一次腮红。但我想知道这种方法是否存在任何潜在的问题,这些问题在表面上可能并不明显。

我对 Rust 的内存模型、编译器优化、机器代码级语义等的理解不够好,在这种情况下可能会导致问题。

即使这种方法适用于当前平台,我也想知道将来是否会遇到有关未定义行为的潜在问题。

优化 Rust 编译器-优化 未定义行为

评论

2赞 Chayim Friedman 10/29/2023
始终在 Miri 下运行不安全的代码。

答:

4赞 Chayim Friedman 10/29/2023 #1

不。如果没有 UnsafeCell,绝对不可能在 Rust 中实现内部可变性

你的代码是UB,Miri把它标记为这样

error: Undefined Behavior: attempting a write access using <1698> at alloc880[0x0], but that tag only grants SharedReadOnly permission for this location
  --> src/main.rs:20:18
   |
20 |         unsafe { std::ptr::write_volatile(ptr, value) }
   |                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   |                  |
   |                  attempting a write access using <1698> at alloc880[0x0], but that tag only grants SharedReadOnly permission for this location
   |                  this error occurs as part of an access at alloc880[0x0..0x4]
   |
   = help: this indicates a potential bug in the program: it performed an invalid operation, but the Stacked Borrows rules it violated are still experimental
   = help: see https://github.com/rust-lang/unsafe-code-guidelines/blob/master/wip/stacked-borrows.md for further information
help: <1698> was created by a SharedReadOnly retag at offsets [0x0..0x4]
  --> src/main.rs:18:19
   |
18 |         let ptr = &self.value as *const T;
   |                   ^^^^^^^^^^^
   = note: BACKTRACE (of the first span):
   = note: inside `CopyCell::<i32>::set` at src/main.rs:20:18: 20:54
note: inside `main`
  --> src/main.rs:51:5
   |
51 |     a.set(123);
   |     ^^^^^^^^^^

通过使用 volatile,你只是欺骗了优化器,所以你的代码“工作”,但它仍然是 UB。

人们确实想要一个 UnsafeCell 作为 Copy,但现在添加它是一个问题,因为人们依赖意味着没有内部。不过,也许有一天会添加它。T: CopyTUnsafeCell

评论

0赞 Ricardo Machado 10/29/2023
谢谢。所以到目前为止,Rust 还没有办法很好地实现我想要的东西。我遇到了一个相关的讨论 github.com/rust-lang/unsafe-code-guidelines/issues/411特别是,github.com/rust-lang/unsafe-code-guidelines/issues/...给出了这种方法可能失败的例子。简而言之,Rust 可以自由地读取 Cell 的任何 & ref,而不受限制,这可以绕过易失性读取。我认为即使是副本也可能无法看到更新的易失性写入。对于我的特定用例,这不是问题,但这通常不起作用。
0赞 Chayim Friedman 10/29/2023
@RicardoMachado 周围的讨论在这里无关紧要。即使我们有这样的类型,它也无济于事。您需要内部可变性,而不是易变性。VolatileCell
0赞 cafce25 10/29/2023 #2

这可能是也可能不是您可接受的解决方法,但您可以泄漏 Cell(或任何其他此类内部可变性结构)并存储,以便您拥有内部可变性,其值是以泄漏其内存为代价的:&'static Cell<T>Copy

#[derive(Copy, Clone)]
pub struct CopyCell<T: 'static> {
    value: &'static Cell<T>,
}

impl<T> CopyCell<T> {
    pub fn new(value: T) -> Self {
        Self {
            value: Box::leak(Box::new(Cell::new(value))),
        }
    }

    pub fn set(&self, value: T) {
        self.value.set(value);
    }
}

impl<T: Copy> CopyCell<T> {
    pub fn get(&self) -> T {
        self.value.get()
    }
}

评论

0赞 Chayim Friedman 10/29/2023
如果你能负担得起分配和间接,你就不需要任何泄漏或内部可变性:你只需持有一个原始指针,就可以允许写入。*mut TCopy
1赞 cdhowie 10/30/2023
这难道不意味着所有副本都共享相同的基础单元格吗?这似乎与OP所要求的语义不同。
0赞 cafce25 10/30/2023
@cdhowie内部可变性仅在共享上下文中才需要,但如果你想要一个拥有的副本,你总是可以使你的绑定可变。
0赞 cdhowie 10/30/2023
@cafce25 这不完全是我的意思。我认为期望是,如果您复制一个,您应该获得一个新拥有的单元格,该单元格的值独立于所有其他副本。但事实并非如此,它在语义上更像是一个 .CellCopyCellArc<Cell>>