#generational-arena #interior-mutability #arena #memory-safe

无 std grit-data-prison

一个提供 Prison<T> 结构体的crate,这是一个允许每个元素都具有完全内部可变性的代际竞技场

3 个不稳定版本

0.4.0 2023年5月5日
0.3.1 2023年5月4日
0.2.3 2023年4月17日
0.1.2 2023年4月9日

#458数据结构

BSD-3-Clause

295KB
3.5K SLoC

Image Image Image Image Image
此 crate 提供 Prison 结构体,这是一个代际竞技场数据结构,通过提供 .visit() 方法来允许对每个元素的值传递可变引用,或通过使用 .guard() 方法来获取对值的受保护可变引用。

本文档描述了 Prison 的用法,其方法与 [Vec] 中的方法有何不同,如何访问其包含的数据以及它是如何实现内存安全的。

grit-data-prison 在 Crates.io
grit-data-prison 在 Lib.rs
grit-data-prison 在 Github
grit-data-prison 在 Docs.rs

快速查看

  • 使用底层 [Vec] 存储相同类型的项
  • 主要作为代际竞技场使用,其中每个元素都使用 [CellKey] 访问,[CellKey] 区分了可能位于相同索引但表示基本不同数据的两个值
  • 也可以使用普通的 [usize] 进行索引,以用于简单场景
  • 通过在每个元素上执行引用计数,提供安全的(需要验证)内部可变性,以符合 Rust 的内存安全规则
  • 在每个元素上使用 [usize] 引用计数器,并使用主 [usize] 计数器跟踪活动引用的数量/位置,以防止可变引用别名和禁止可能使现有引用无效的情况
  • 【CellKey】使用[usize]索引和[usize]生成,将索引与其创建的上下文匹配,以防止两个无关的值在某个时刻位于同一索引处被错误地认为是相等的
  • 所有方法都返回一个[AccessError],如果不捕获,则可能导致panic

注意

此包仍然是不稳定的,我可能还需要进行几次迭代,才能将其固定下来。请参阅变更日志

  • 版本0.4.x是对0.3.x及更早版本的(非常微小)API更改,但它不会影响大多数用户,并且更新很容易
  • 此外:版本0.2.x及更早版本在使用insert_at()overwrite()时被发现存在软内存泄漏,请参阅变更日志以获取详细信息

动机

我想找到一个满足以下标准的数据结构

  • 由[Vec](或类似)支持以提高缓存效率
  • 允许每个元素都具有内部可变性
  • 完全内存安全(需要验证
  • 始终返回相关错误而不是panic
  • 与引用计数相比,更容易推断其何时何地可能出错

用法

此crate位于crates.io

首先,将此crate作为依赖项添加到您的项目中

[dependencies]
grit-data-prison = "0.3"

然后,从crate根目录导入[AccessError]和[CellKey],以及您希望在该文件中使用的相关版本(目前只有一种风味,[single_threaded])

use grit_data_prison::{AccessError, CellKey, single_threaded::Prison};

创建一个Prison,并使用一个insert()类型方法将其数据添加到其中

注意以下怪癖

  • 不需要将Prison声明为mut即可修改它
  • insert()及其变体返回一个[Result]<[CellKey], [AccessError]>,您需要处理它
  • 如果您愿意,可以忽略[CellKey],只需通过索引查找值即可(稍后展示)
let prison: Prison<String> = Prison::new();
let key_hello = prison.insert(String::from("Hello, "))?;
prison.insert(String::from("World!"))?;

从现在起,访问包含在Prison中的值有2种主要方式

访问监狱中的值

您可以使用.visit()方法之一来访问在闭包内部的数据的不可变引用或可变引用,要么可变,要么不可变

prison.visit_mut_idx(1, |val_at_idx_1| {
    *val_at_idx_1 = String::from("Rust!!");
    Ok(())
});

可变或不可变引用的规则与Rust的正常变量引用规则相同

  • 只有一个可变引用
  • 或任意数量的不可变引用

同时访问多个值可以通过嵌套.visit()调用或使用批处理.visit()方法来完成

prison.visit_ref(key_hello, |val_0| {
    prison.visit_ref_idx(1, |val_1| {
        println!("{}{}", *val_0, *val_1); // Prints "Hello, Rust!!"
        Ok(())
    });
    Ok(())
});
prison.visit_many_ref_idx(&[0, 1], |vals| {
    println!("{}{}", vals[0], vals[1]); // Also prints "Hello, Rust!!"
    Ok(())
});

完整访问示例代码

use grit_data_prison::{AccessError, CellKey, single_threaded::Prison};

fn main() -> Result<(), AccessError> {
    let prison: Prison<String> = Prison::new();
    let key_hello = prison.insert(String::from("Hello, "))?;
    prison.insert(String::from("World!"))?;
    prison.visit_mut_idx(1, |val_at_idx_1| {
        *val_at_idx_1 = String::from("Rust!!");
        Ok(())
    });
    prison.visit_ref(key_hello, |val_0| {
        prison.visit_ref_idx(1, |val_1| {
            println!("{}{}", *val_0, *val_1); // Prints "Hello, Rust!!"
            Ok(())
        });
        Ok(())
    });
    prison.visit_many_ref_idx(&[0, 1], |vals| {
        println!("{}{}", vals[0], vals[1]); // Also prints "Hello, Rust!!"
        Ok(())
    });
    Ok(())
}

使用包装结构体保护值

您还可以使用.guard()方法之一来获取围绕您的数据的受保护包装,只要包装保持作用域,就会标记值为引用。

首先,您需要从与Prison相同的模块中导入以下之一:PrisonValueMutPrisonValueRefPrisonSliceMutPrisonSliceRef

use grit_data_prison::{AccessError, CellKey, single_threaded::{Prison, PrisonValueRef}};

然后,通过使用对应的.guard()方法获取一个受保护的包装器

let prison: Prison<String> = Prison::new();
let key_hello = prison.insert(String::from("Hello, "))?;
prison.insert(String::from("World!"))?;
let grd_hello = prison.guard_ref(key_hello)?;

只要不违反引用规则,您就可以保护(或访问)该值,即使其他来自同一监狱的值正在被访问或保护。受保护的包装器(例如PrisonValueRef)将标记的元素(或元素集)保留在适当的引用形式,直到它们超出作用域。这可以通过将使用区域包装在代码块中或通过手动传递给包装器类型的关联::unguard()函数来实现,以立即将其超出作用域并更新引用计数。

受保护的包装器类型都实现了[Deref]、[AsRef]和[Borrow],而可变版本还实现了[DerefMut]、[AsMut]和[BorrowMut],以提供对内部值的透明访问

{
    let grd_hello = prison.guard_ref(key_hello)?;
    let grd_world = prison.guard_ref_idx(1)?;
    println!("{}{}", *grd_hello, *grd_world); // Prints "Hello, World!"
}
// block ends, both guards go out of scope and their reference counts return to what they were before
let mut grd_world_to_rust = prison.guard_mut_idx(1)?;
*grd_world_to_rust = String::from("Rust!!");
PrisonValueMut::unguard(grd_world_to_rust); // index one is no longer marked mutably referenced
let grd_both = prison.guard_many_ref_idx(&[0, 1])?;
println!("{}{}", grd_both[0], grd_both[1]); // Prints "Hello, Rust!!"

完整保护示例代码

use grit_data_prison::{AccessError, CellKey, single_threaded::{Prison, PrisonValueRef, PrisonValueMut, PrisonSliceRef}};

fn main() -> Result<(), AccessError> {
    let prison: Prison<String> = Prison::new();
    let key_hello = prison.insert(String::from("Hello, "))?;
    prison.insert(String::from("World!"))?;
    {
        let grd_hello = prison.guard_ref(key_hello)?;
        let grd_world = prison.guard_ref_idx(1)?;
        println!("{}{}", *grd_hello, *grd_world); // Prints "Hello, World!"
    }
    // block ends, both guards go out of scope and their reference counts return to what they were before
    let mut grd_world_to_rust = prison.guard_mut_idx(1)?;
    *grd_world_to_rust = String::from("Rust!!");
    PrisonValueMut::unguard(grd_world_to_rust); // index one is no longer marked mutably referenced
    let grd_both = prison.guard_many_ref_idx(&[0, 1])?;
    println!("{}{}", grd_both[0], grd_both[1]); // Prints "Hello, Rust!!"
    Ok(())
}

影响底层[Vec]的操作也可以在.visit()闭包内部或当值被guard()时进行,只要不违反以下规则:

  • 操作不会移除、读取或修改任何当前正在被引用的元素
  • 操作不会导致整个[Vec]重新分配(或以其他方式导致整个[Vec]移动到另一个内存地址)
let prison: Prison<u64> = Prison::with_capacity(5);
prison.insert(0)?;
prison.insert(10)?;
prison.insert(20)?;
prison.insert(30)?;
prison.insert(42)?;
let mut accidental_val: u64 = 0;
let mut grd_0 = prison.guard_mut_idx(0)?;
prison.visit_ref_idx(3, |val| {
    accidental_val = prison.remove_idx(4)?;
    prison.insert(40)?;
    Ok(())
});
*grd_0 = 80;
PrisonValueMut::unguard(grd_0);
// No values are actively referenced here so we can perform
// an action that would cause re-allocation safely
for i in 0..100u64 {
    prison.insert(i + 100)?;
}

还提供了一个快速快捷方式来克隆类型T实现[Clone]时的Prison中的值。因为克隆值不会更改原始值或假设有关值内容的任何先决条件,所以在单线程上下文中克隆当前正在被保护或访问的值是安全的。

示例

let prison: Prison<String> = Prison::new();
let key_0 = prison.insert(String::from("Foo"))?;
prison.insert(String::from("Bar"))?;
let cloned_foo = prison.clone_val(key_0)?;
let cloned_bar = prison.clone_val_idx(1)?;

有关更多示例,请参阅相关类型/方法的特定文档

监狱

还包括结构体JailCell,它是一个独立的Prison版本,但没有生成计数器。

JailCell包括与Prison相同的接口,并也使用引用计数,但具有更简单的一组安全检查。

use grit_data_prison::{AccessError, CellKey, single_threaded::{JailCell, JailValueRef}};

fn main() -> Result<(), AccessError> {
    let string_jail: JailCell<String> = JailCell::new(String::from("'Bad-Guy' Bert"));
    string_jail.visit_mut(|criminal| {
        let bigger_bad = String::from("Dr. Lego-Step");
        println!("Breaking News: {} to be set free to make room for {}", *criminal, bigger_bad);
        *criminal = bigger_bad;
        Ok(())
    })?;
    let guarded_criminal = string_jail.guard_ref()?;
    println!("{} will now be paraded around town for public shaming", *guarded_criminal);
    assert_eq!(*guarded_criminal, String::from("Dr. Lego-Step"));
    JailValueRef::unguard(guarded_criminal);
    Ok(())
}

有关更多信息,请参阅JailCell的文档

为什么这种语法如此奇怪?

对于visit()方法,闭包提供了访问可变引用的安全沙盒,因为它们不能从闭包中移动,并且因为visit()函数在处理闭包时处理所有所需的安全和家务工作。

由于闭包使用泛型,Rust编译器可以在许多/大多数/所有情况下内联它们。

guard()方法需要值不能泄漏、别名或从未重置它们的引用计数,因此它们被包装在结构体中,这些结构体提供了对引用的有限访问并知道如何在它们超出作用域时自动重置值的引用计数。

这怎么是安全的?!

简短回答是:它应该是大部分安全的。我欢迎任何反馈和分析,以显示否则,这样我就可以修复它或修改我的方法。

监狱遵循一些简单规则

  • 只有当值具有零引用或只有不可变引用时,才能获得不可变引用
  • 只有当值的任何类型引用为零时,才能获得可变引用
  • 任何可能或可能读取、修改或删除任何元素的方法,在元素当前被引用时无法执行
  • 任何可能导致底层 [Vec] 移动到内存中不同位置的方法,在 [Vec] 中任何元素的任何引用仍在作用域内时无法执行

此外,它还提供了具有以下附加规则的代际竞技场功能

  • 监狱有一个主代际计数器来跟踪其中任何元素的最大代数
  • 每个有效元素都附加一个代数,并且 insert() 操作返回一个 [CellKey],将元素索引与当前最大代数值配对
  • 任何删除 用具有等于最大代数的 生成器计数器 替换有效元素的操作会导致主生成器计数器增加一个

它通过一些轻量级哨兵值实现了上述所有功能

  • 一个单独的 UnsafeCell 来持有 所有监狱 内部组件,并提供内部可变性
  • 监狱 本身上有一个主 access_count [usize],用于跟踪是否有任何引用处于活动状态
  • 每个元素基本上是一个 CellFree 变体
    • Free 元素充当跟踪空闲索引的双向链表中的节点
      • 一个指向在释放之前创建的此空闲索引的前一个空闲索引的 [usize]
      • 一个指向在填充之后此空闲索引的下一个空闲索引的 [usize]
    • 单元
      • 一个跟踪可变和不可变引用的 ref_count [usize]
      • 一个用于与访问索引时使用的 [CellKey] 匹配的 generation [usize]
      • 一个类型为 T 的值

(有关实际细节的更多信息,请参阅性能)

尝试执行违反这些规则之一的操作将无法编译,或返回一个描述为什么它是错误的 [AccessError],并且永远不会发生恐慌。

示例:编译时安全

let prison: Prison<String> = Prison::new();
prison.insert(String::from("cannot be stolen"));
let mut steal_mut_ref: &mut String;
let mut steal_prison: Prison<String>;
prison.visit_mut_idx(0, |mut_ref| {
    // will not compile: (error[E0521]: borrowed data escapes outside of closure)
    steal_mut_ref = mut_ref;
    // will not compile: (error[E0505]: cannot move out of `prison` because it is borrowed)
    steal_prison = prison;
    Ok(())
});

示例:运行时安全

struct MyStruct(u32);

fn main() -> Result<(), AccessError> {
    let prison: Prison<MyStruct> = Prison::with_capacity(2); // Note this prison can only hold 2 elements
    let key_0 = prison.insert(MyStruct(1))?;
    prison.insert(MyStruct(2))?;
    let grd_0 = prison.guard_mut(key_0)?;
    assert!(prison.guard_mut(key_0).is_err());
    assert!(prison.guard_ref_idx(0).is_err());
    PrisonValueMut::unguard(grd_0);
    prison.visit_mut(key_0, |val_0| {
        assert!(prison.visit_mut(key_0, |val_0_again| Ok(())).is_err());
        assert!(prison.visit_ref(key_0, |val_0_again| Ok(())).is_err());
        assert!(prison.visit_mut_idx(0, |val_0_again| Ok(())).is_err());
        assert!(prison.visit_ref_idx(3, |val_3_out_of_bounds| Ok(())).is_err());
        assert!(prison.guard_mut(key_0).is_err());
        assert!(prison.guard_ref(key_0).is_err());
        assert!(prison.guard_ref_idx(3).is_err());
        prison.visit_ref_idx(1, |val_1| {
            assert!(prison.remove_idx(1).is_err()); // would delete memory referenced by val_1
            assert!(prison.remove(key_0).is_err()); // would delete memory referenced by val_0
            assert!(prison.insert(MyStruct(3)).is_err()); // would cause reallocation and invalidate any current references
            Ok(())
        });
        Ok(())
    });
    Ok(())
}

库功能

no_std:此库可以与 no_std 功能一起使用,仅使用来自 core 库的导入,而不是 std

主要故障
此库可以通过三种(可选)功能之一传递,这些功能定义了库如何处理确定不是预期行为且应被视为库本身错误的行为。如果没有指定,则默认为 major_malf_is_err

  • major_malf_is_err:主要故障将作为 [AccessError::MAJOR_MALFUNCTION(msg)] 返回,这是默认值,即使未指定也是如此
  • major_malf_is_panic:主要故障将导致调用 panic(msg) 描述意外行为
  • major_malf_is_undefined:在通常会发生主要故障的地方,分支被 [unreachable_unchecked()] 替换,这可能会允许它们完全从编译中删除

性能

速度

(基准测试即将推出™)

大小

监狱(Prison)除了包含一个 [Vec] 外,还有 4 个 [usize] 的家务值。

尽管每个 PrisonCell<T> 的抽象如如何保证安全性?!中所述,但实际情况是 Rust 并没有在可以使用枚举的情况下优化内存占用,因此我不得不自己实现枚举类型。

  • 每个元素都是一个结构体,具有自定义的 CellFree 变体,变体在其中一个字段的最高位跟踪。
    • 字段 refs_or_next 保存一个 [usize],该值在 Cell 变体中保存引用计数,在 Free 变体中保存下一个空闲值。
    • 字段 d_gen_or_prev 保存一个 [usize],该值在 Cell 变体中保存生成计数,在 Free 变体中保存前一个空闲值。
      • 此外,d_gen_or_prev 的最高位保留用于标记 PrisonCell 的变体("d" 代表 "discriminant")。这意味着实际的 最大 生成计数是 isize::MAX,但前一个索引不受影响,因为 [Vec] 不可能有超过 isize::MAX 个元素...
    • 字段 val 是一个 MaybeUninit<T>,当元素处于 Free 状态时始终假定未初始化,当处于 Cell 状态时始终假定已初始化。

因此,与 [Vec] 相比,总 额外 大小在 64 位系统上为 32 字节 + 每个元素 16 字节,这些值在测试套件中通过一个可选的测试进行了验证,该测试检查了几种类型 Tmem::size_of

此软件包可能如何变化

此软件包非常不稳定,这意味着可能不会测试每个错误条件,方法可能会返回不同的错误/值,因为我对它们应该如何正确实现的了解不断演变,我可能会添加/删除方法,等等。

可能未来的添加包括

  • 单线程安全的 Prison
  • Guard API,以更符合 Rust 习惯的方式访问值
  • 切换到具有相同内存占用的引用计数
  • 常量泛型界限以自定义内部实用值的大小
  • 更多公共方法(只要它们有意义且不会膨胀 API)
  • 多线程安全的 AtomicPrison<T>
  • ? 单独的独立值版本,JailCell
  • ? 多线程安全的独立值版本,AtomicJailCell<T>
  • ?? 完全未检查和不安全版本 UnPrison<T>
  • ??? 多线程 安全 不安全版本 AtomicUnPrison<T>

如何帮助/贡献

此软件包位于 crates.io 上,仓库位于 github 上。

欢迎提出反馈,或者分叉/创建分支并提交修复/优化方案!

如果您能提供具体的示例,这些示例明确违反了内存安全性,即提供的引用可以指向无效/非法内存或违反别名规则(不使用额外的非安全操作:P),泄露内存,或者造成其他不安全条件(例如,将期望的枚举变体更改为编译器不期望其可能的情况),我会很高兴修复、进一步限制或完全重新思考这个库。

最好的方法是按照以下步骤操作

  • dev 分支创建一个 bug/somethingissue/something 分支
  • 创建一个新测试,以演示库当前存在的失败情况
  • 然后执行以下操作之一
    • 在您的分支中解决问题,并向 dev 分支提交一个带有解释一切信息的拉取请求
    • 创建一个只包含证明失败点的测试的拉取请求,并在消息中描述为什么这是一个失败,以及 这个拉取请求并没有解决问题

变更日志

  • 版本 0.4.0:重大变更:将 peek_ref()peek_ref_idx() 的返回值从 [Option] 改为 [Result<T, AccessError>],并将 peek_ref() 添加到 JailCell
    • 我知道这只是一个非常小的差异,但破坏就是破坏,抱歉!它应该从一开始就返回 Result,以匹配现有的 API 并允许在期望 AccessError 的函数中轻松传递错误,而无需一大堆为 Some/None 进行样板测试,以返回一个 AccessError::ValueDeleted
  • 版本 0.3.1:非破坏性功能:peek_ref()peek_ref_idx(),不安全的函数,允许调用者在不绕过引用计数和其他安全检查的情况下获取一个值的引用
  • 版本 0.3.0:API的重大破坏性变更
    • 改为使用引用计数而不是 [bool] 锁:在大多数情况下内存占用相同,安全逻辑几乎相同。引用计数提供了更多的灵活性,并且比使用 [bool] 具有更细粒度的控制,没有实际惩罚
    • escort() 方法重命名为 guard() 方法
    • visit()guard() 方法分为 _ref()_mut() 变体
    • [AccessError] 变体重命名并改为更清晰的表达
    • 添加了 3 个 crate 功能:major_malf_is_errmajor_malf_is_panicmajor_malf_is_undefined,允许为 确定 是库中错误的操作进行条件编译选择
    • 版本0.2.x及更低版本被发现存在软内存泄漏,应避免使用:当在非栈顶的空闲索引上使用insert_at()overwrite()时,所有高于它们的空闲索引在栈中都将被遗忘且不会再次使用。然而,当整个Prison被释放时,它们应该被释放。抱歉!
  • 版本0.2.3:非破坏性特性:添加了clone_val()方法,用于在T实现[Clone]时简化值的克隆
  • 版本0.2.2:对PrisonValuePrisonSlice的非破坏性更新,以减少它们的内存占用
  • 版本0.2.1:添加了非破坏性的escort() API函数(为什么我之前没想到这个?)
  • 版本0.2.x:与版本0.1.x的API不同,是从简单的Vec迁移到Generational Arena
  • 版本0.1.x:第一个版本,使用[usize]索引的普通Vec

无运行时依赖

特性