9次发布
0.6.5 | 2023年11月13日 |
---|---|
0.6.4 | 2023年2月20日 |
0.5.2 | 2023年2月18日 |
#79 in 内存管理
每月 24 次下载
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::OsStr
和 std::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