#bump-allocator #bump #allocation #allocator #arena #free-memory

no-std bump-scope

支持分配范围的快速bump分配器。又称任意类型值的竞技场。

25个版本 (5个重大变更)

新功能 0.5.8 2024年8月22日
0.5.5 2024年7月26日
0.1.2 2024年3月29日

#46 in 内存管理

Download history 19/week @ 2024-05-03 375/week @ 2024-05-17 24/week @ 2024-05-24 2/week @ 2024-05-31 304/week @ 2024-06-07 14/week @ 2024-06-14 176/week @ 2024-06-28 4/week @ 2024-07-05 200/week @ 2024-07-26 10/week @ 2024-08-02 341/week @ 2024-08-16

551 每月下载量

MIT/Apache

690KB
12K SLoC

bump-scope

Crates.io Documentation Rust License Build Status

支持分配范围的快速bump分配器。又称任意类型值的竞技场。

什么是bump分配?

bump分配器拥有一大块内存。它有一个指针从那块内存的一端开始。当进行分配时,该指针根据分配的大小对齐并推向另一端。当它的内存块满时,它会分配一个两倍大小的内存块。

这使得分配非常快。缺点是您不能像使用更通用的分配器那样回收内存。最新分配的内存可以回收。您还可以使用范围、检查点reset来回收内存。

bump分配器非常适合面向阶段的分配,其中您在循环中分配对象,并在每次迭代的末尾释放它们。

use bump_scope::Bump;
let mut bump: Bump = Bump::new();

loop {
    // use bump ...
    bump.reset();
}

由于bump分配器分配的内存块越来越大,而reset只保留最大的一个,因此在几次迭代后,每次bump分配都将使用同一个内存块,不再需要分配新的内存块。

范围的概念引入使得这个bump分配器也非常适合临时分配和栈式使用。

bumpalo的比较

Bumpalo是流行的bump分配crates。这个crates受到bumpalo和Always Bump Downwards的启发。

bumpalo不同,这个crates...

  • 支持范围和检查点
  • 除非显式泄漏或忘记,否则始终调用分配值的Drop。
    • alloc*方法返回一个拥有和丢弃TBumpBox<T>。不需要丢弃的类型可以使用into_refinto_mut转换为引用。
  • 您可以使用 alloc_iter任何 Iterator 分配一个切片。
  • 每个在分配失败时引发恐慌的方法都有一个不可靠的 try_* 对应方法。
  • Bump 的基本分配器是通用的。
  • BumpBumpScope 的表示与 NonNull<u8> 相同。(与3倍指针大小相比)
  • 如果分配失败,不会尝试分配更小的块。
  • 没有内置的分配限制。您可以为分配限制提供分配器(请参阅 tests/limit_memory_usage.rs)。
  • 分配进行了更多的优化。(请参阅 crates/inspect-asm/out/x86-64基准测试
  • 您可以选择 bump 的方向。 默认向上 bump。
  • 您可以选择最小对齐。

作用域和检查点

您可以通过创建作用域来创建仅存在于其父作用域部分的生命周期的分配。创建和退出作用域几乎免费。在作用域内进行分配没有开销。

您可以使用 scoped 闭包或 scope_guard 创建一个新的作用域。

use bump_scope::Bump;

let mut bump: Bump = Bump::new();

// you can use a closure
bump.scoped(|mut bump| {
    let hello = bump.alloc_str("hello");
    assert_eq!(bump.stats().allocated(), 5);
    
    bump.scoped(|bump| {
        let world = bump.alloc_str("world");

        println!("{hello} and {world} are both live");
        assert_eq!(bump.stats().allocated(), 10);
    });
    
    println!("{hello} is still live");
    assert_eq!(bump.stats().allocated(), 5);
});

assert_eq!(bump.stats().allocated(), 0);

// or you can use scope guards
{
    let mut guard = bump.scope_guard();
    let mut bump = guard.scope();

    let hello = bump.alloc_str("hello");
    assert_eq!(bump.stats().allocated(), 5);
    
    {
        let mut guard = bump.scope_guard();
        let bump = guard.scope();

        let world = bump.alloc_str("world");

        println!("{hello} and {world} are both live");
        assert_eq!(bump.stats().allocated(), 10);
    }
    
    println!("{hello} is still live");
    assert_eq!(bump.stats().allocated(), 5);
}

assert_eq!(bump.stats().allocated(), 0);

您还可以使用不安全的 checkpoint API 将 bump 指针重置到先前的位置。

let checkpoint = bump.checkpoint();

{
    let hello = bump.alloc_str("hello");
    assert_eq!(bump.stats().allocated(), 5);
}

unsafe { bump.reset_to(checkpoint); }
assert_eq!(bump.stats().allocated(), 0);

集合

bump-scope 提供了 VecString 的 bump 分配版本,分别称为 BumpVecBumpString。它们也有不同的风味

  • Fixed* 用于固定容量集合
  • Mut* 用于针对可变 bump 分配器优化的集合

并行分配

Bump!Sync,这意味着它不能在线程之间共享。

要在并行中进行 bump 分配,您可以使用 BumpPool

分配器 API

BumpBumpScope 实现了 allocator_api2::alloc::Allocator。使用此功能,您可以 bump 分配 allocator_api2::boxed::Boxallocator_api2::vec::Vec 以及支持它的其他包中的集合,如 hashbrown::HashMap。它们还实现了带有 功能标志 的夜间分配器 API。

bump 分配器可以增长、缩小和释放最近的分配。在向上 bump 时,它甚至可以就地执行。除了最近的分配之外的增长分配将需要一个新分配,并且旧内存块变成浪费的空间。缩小或释放除了最近的分配之外的其他分配没有任何作用,这意味着浪费空间。

跳跃分配器不需要释放shrink来释放内存。毕竟,当退出作用域或调用reset时,内存将被回收。您可以使用without_deallocwithout_shrink将跳跃分配器包装在一种类型中,使deallocateshrink成为空操作。

use bump_scope::Bump;
use allocator_api2::boxed::Box;
let bump: Bump = Bump::new();

let boxed = Box::new_in(5, &bump);
assert_eq!(bump.stats().allocated(), 4);
drop(boxed);
assert_eq!(bump.stats().allocated(), 0);

let boxed = Box::new_in(5, bump.without_dealloc());
assert_eq!(bump.stats().allocated(), 4);
drop(boxed);
assert_eq!(bump.stats().allocated(), 4);

特性标志

  • std(默认启用)— 添加了BumpPoolstd::io trait的BumpBox{Fixed, Mut}BumpVec实现。
  • alloc— 添加了默认分配器GlobalBumpBox::into_box和一些与alloc集合的交互。
  • serde— 为BumpBox、字符串和向量添加了Serialize实现,并为字符串和向量添加了DeserializeSeed
  • zerocopy— 添加了alloc_zeroed(_slice)init_zeroedresize_zeroedextend_zeroed

夜间功能

  • nightly-allocator-api— 启用allocator-api2nightly特性,使其重新导出夜间分配器API而不是自己的实现。有了这个特性,您可以从标准库中分配跳跃分配器集合。
  • nightly-coerce-unsized— 使BumpBox<T>实现CoerceUnsized。有了这个特性,BumpBox<[i32;3]>可以转换为BumpBox<[i32]>BumpBox<dyn Debug>等等。
  • nightly-const-refs-to-static— 使Bump::unallocated成为一个const fn
  • nightly-exact-size-is-empty— 为某些迭代器手动实现is_empty
  • nightly-trusted-len— 为某些迭代器实现TrustedLen

向上还是向下跳跃?

跳跃方向由泛型参数const UP: bool控制。默认情况下,UPtrue,因此分配器向上跳跃。

  • 向上跳跃...
    • 优点是最近的分配可以原地增长和缩小。
    • 使alloc_iter(_mut)alloc_fmt(_mut)更快。
  • 向下跳跃...
    • 每个分配使用的指令略少。
    • 使alloc_iter_mut_rev更快。

最小对齐?

最小对齐方式由泛型参数 const MIN_ALIGN: usize 控制。默认情况下,MIN_ALIGN1

将最小对齐方式更改为例如 4,使得对齐方式为 4 的分配不再需要对指针进行对齐。这将惩罚更小对齐方式的分配,因为它们的大小现在需要向上取到 4 的下一个倍数。

这相当于每个分配大约有 1 或 2 个非分支汇编指令。

GUARANTEED_ALLOCATED 参数?

GUARANTEED_ALLOCATEDtrue 时,增量分配器保证至少有一个已分配的块。这通常是情况,除非你使用 Bump::unallocated 创建它。

你需要一个保证已分配的 Bump(Scope) 来通过 scopedscope_guard 创建作用域。你可以使用 into_guaranteed_allocatedas_guaranteed_allocated(_mut) 将可能未分配的 Bump(Scope) 转换为保证已分配的。

这样做的目的是让 Bump 可以在不分配内存的情况下创建,即使在启用功能 nightly-const-refs-to-static 时也可以进行 const 构造。同时,已经分配了一个块的 Bump 不需要在进入作用域和创建检查点时进行运行时检查。

测试

运行 cargo test 需要一个夜间编译器。这是因为我们使用了从 std 复制的测试,这些测试大量使用了夜间功能。

许可

根据您的选择,许可如下:

您的贡献

除非您明确声明,否则根据 Apache-2.0 许可证定义的任何贡献,都应作为上述双重许可,不附加任何额外的条款或条件。

依赖关系

~300–640KB