#pointers #box #alloc #cache #raw-pointers #memory-size #no-std

no-std slimmer_box

Box<T>的紧凑替代品,其'胖指针'更'瘦'

9次发布

0.6.5 2023年11月13日
0.6.4 2023年2月20日
0.5.2 2023年2月18日

#79 in 内存管理

每月 24 次下载

MIT 许可证

72KB
1K SLoC

slimmer_box   最新版本 许可证

SlimmerBox<T>是Box<T>的紧凑替代品,其'胖指针'更'瘦'

文档

理由

一个普通的Box<[T]>是一个包含指向内存的'原始'指针以及管理的切片大小(作为一个usize)的所有权'胖指针'。

在64位目标上(其中sizeof(usize) == sizeof(u64)),这使得Box<[T]>占用16字节(128位,2个字)。这很遗憾:这意味着如果你创建一个包含Box<[T]>的枚举,那么它至少需要24字节(196位,3个字)的栈内存。

但通常我们处理的是永远不会那么大的切片。例如,如果我们把大小存储在一个u32中呢?你的切片真的会包含超过2ˆ32(4_294_967_296)个元素吗?一个[u8; 2^32]占用4GiB的空间。

由于长度按元素计数,所以一个 [u64; 2^32] 占用 32GiB。

所以让我们把这个“胖”指针瘦身!通过将长度存储在 u32 而不是 u64 中,SlimmerBox<[T], u32> 只占用 12 字节(96 位,1.5 个字),而不是 16 字节。

这使得它可以用于另一个结构中,例如在枚举的一个或多个变体中。然后生成的结构仍然只占用 16 字节。

在尝试优化内存使用、缓存局部性等情况时,这可能会产生影响

激励示例

下面的“小型字符串优化”枚举仍然只占用两个字,就像一个正常的 &str 一样

use slimmer_box::SlimmerBox;
pub enum CompactStr {
    Small{buffer: [u8; 14], len: u8}, // <- Or, using the `modular_bitfield` crate, this could even be { buffer: [u8; 15], len: u4} !
    Large{ptr: SlimmerBox<str>},
}

impl From<&str> for CompactStr {
    fn from(val: &str) -> CompactStr {
        if val.len() < 14 {
            let len = val.len() as u8;
            let mut buffer = [0u8; 14];
            buffer[0..val.len()].copy_from_slice(val.as_bytes());
            CompactStr::Small{ buffer, len }
        } else {
            CompactStr::Large{ ptr: SlimmerBox::new(val) }
        }
    }
}

let compact_str: CompactStr = "hello world".into();
assert_eq!(core::mem::size_of_val(&compact_str), 16);

// An Option<CompactStr> also only takes up two words:
assert_eq!(core::mem::size_of_val(&Some(compact_str)), 16);

(包括 Debug、Display 和 Deref 特性的完整示例可以在 这个测试 中找到)

下面的不可变 AST 仍然只占用两个字。即使是 Option<AST> 也只占用两个字

pub enum AST {
    Bool(bool),
    Int(i64),
    Float(f64),
    Str(SlimmerBox<str>),
    Bytes(SlimmerBox<[u8]>),
    List(SlimmerBox<[AST]>),
    // 2^32 - 7 other variants could be added and the size would still stay the same :-)
}
assert_eq!(core::mem::size_of::<AST>(), 16);
assert_eq!(core::mem::size_of::<Option<AST>>(), 16);

经过一些细心处理,您甚至可以将上述两个示例结合起来,最终得到的 AST 类型仍然只占用两个字!

不同的大小

SlimmerBox<T, u32> 是最常用的版本,因此 u32 是默认的 SlimmerMetadata。但如果有信心数据会更短,也可以使用另一个变体。

  • SlimmerMetadata = () 用于有大小类型。在这种情况下,SlimmerBox 只包含普通指针,正好是 1 个字大小,就像包含有大小类型的普通 Box 一样。
  • SlimmerMetadata = u64 会使 SlimmerBox 在 64 位系统上表现得就像包含动态大小类型的普通 Box。
SlimmerMetadata 最大 DST 长度¹ 结果大小(32位) 结果大小(64位) 注意
() - 4 字节 8 字节 用于正常大小的类型。在这种情况下与普通 Box 的大小相同。
u8 255 5 字节 9 字节
u16 65535 6 字节 10 字节 等同于 16 位系统上的 Box
u32 4294967295 8 字节(2 个字) 12 字节 等同于 32 位系统上的 Box
u64 18446744073709551615 16 字节(2 个字) 等同于 64 位系统上的 Box
  • ¹ 最大 DST 长度对于 str 是字节,对于切片是元素数量。

利基优化

就像普通 Box 一样,sizeof(Option<SlimmerBox<T>>) == sizeof(SlimmerBox<T>)

Rkyv

SlimmerBox 已实现 rkyv 的存档、序列化和反序列化。SlimmerBox 的序列化版本只是一个普通的 rkyv::ArchivedBox<[T]>。这真是一对天作之合,因为 rkyv 的相对指针只使用 32 位来表示指针部分以及长度部分。因此,sizeof(rkyv::Archived<SlimmerBox<T>>) == 8 字节 (!)。 (这假设使用了 rkyv 的 size_32 功能,这是默认设置。将其更改为 size_64 很少有用,原因与上述关于长度的抱怨相同。)

局限性

您不能使用 SlimmerBox 来存储 trait 对象。这是因为 dyn 指针的元数据是另一个完整大小的指针。我们无法将其缩小!

no_std 支持

SlimmerBox 在 no_std 环境中运行得非常好,只要 alloc 包可用。

(在 no_std 环境中缺少的是 std::ffi::OsStrstd::ffi::CStr 的 SlimmerPointee 实现,这两个实现都不存在于禁用 std 的情况下。)

使用示例

(以下示例假设为 64 位系统)

对于动态大小的类型(如切片或字符串)来说,比普通的 Box 小

use slimmer_box::SlimmerBox;

let array: [u64; 4] = [1, 2, 3, 4];

let boxed_slice: Box<[u64]> = Box::from(&array[..]);
assert_eq!(core::mem::size_of_val(&boxed_slice), 16);

let slimmer_boxed_slice: SlimmerBox<[u64]> = SlimmerBox::new(&array[..]);
assert_eq!(core::mem::size_of_val(&slimmer_boxed_slice), 12);

对于普通、已大小的类型来说,与普通 Box 相同

use slimmer_box::SlimmerBox;

let int = 42;

let boxed_int = Box::new(&int);
assert_eq!(core::mem::size_of_val(&boxed_int), 8);

let slimmer_boxed_int: SlimmerBox<u64, ()> = SlimmerBox::new(&int);
assert_eq!(core::mem::size_of_val(&slimmer_boxed_int), 8);

您可以配置动态大小的切片或字符串长度所需的空间量

use slimmer_box::SlimmerBox;

let array: [u64; 4] = [1, 2, 3, 4];
// Holds at most 255 elements:
let tiny: SlimmerBox<[u64], u8>  = SlimmerBox::new(&array);
assert_eq!(core::mem::size_of_val(&tiny), 9);

// Holds at most 65535 elements or a str of 64kb:
let small: SlimmerBox<[u64], u16>  = SlimmerBox::new(&array);
assert_eq!(core::mem::size_of_val(&small), 10);

// Holds at most 4294967295 elements or a str of 4GB:
let medium: SlimmerBox<[u64], u32>  = SlimmerBox::new(&array);
assert_eq!(core::mem::size_of_val(&medium), 12);

// Holds at most 18446744073709551615 elements, or a str of 16EiB:
let large: SlimmerBox<[u64], u64>  = SlimmerBox::new(&array); // <- Indistinguishable from a normal Box
assert_eq!(core::mem::size_of_val(&large), 16);

您可以将 Box 转换为 SlimmerBox,反之亦然

use slimmer_box::SlimmerBox;

let message = "hello, world!";
let boxed = Box::new(message);
let slimmer_box = SlimmerBox::from_box(boxed);
let again_boxed = SlimmerBox::into_box(slimmer_box);

功能标志

  • "std"。默认启用。禁用默认功能以在 no_std 环境中使用包。slimmer_box 需要 alloc 包可用。
  • "rkyv"。启用对 rkyv 零拷贝序列化/反序列化库的支持,该库非常适合此包!
  • "serde"。启用对 serde 序列化/反序列化库的支持。

MSRV

slimmer_box 所支持的最小 Rust 版本是 1.58.1。

依赖关系

~1.2–1.7MB
~41K SLoC