4次发布
0.3.0 | 2022年7月21日 |
---|---|
0.2.2 | 2022年7月2日 |
0.2.1 | 2022年7月1日 |
0.2.0 | 2022年7月1日 |
0.1.0 |
|
#509 in 嵌入式开发
每月243次下载
在2个包中使用(通过usbd-human-interface-devi…)
25KB
295 行
可选块!
option-block
包提供了一种简单的原始类型,用于固定大小的可选类型块。从正式的角度来说,它是一个直接地址表,以固定大小的数组作为存储介质。
重要的是,这不要与流行的slab
包混淆,它内部使用动态大小的堆分配Vec
。尽管这两个包都提供了索引访问和类似映射的功能,但option-block
是在更低级别上操作的。
具体来说,option-block
在插入时不跟踪分配中的下一个空槽(与slab
不同)。相反,option-block
只是数组和位掩码的包装器。数组包含(可能未初始化的)数据,而位掩码跟踪分配中有效的(即已初始化的)条目。再次强调,它基本上是一个直接地址表。
此包与
no_std
环境兼容!既不需要std
也不需要alloc
。
示例
let mut block = option_block::Block8::<u8>::default();
assert!(block.is_empty());
assert!(block.insert(0, 10).is_none());
assert!(block.insert(1, 20).is_none());
assert_eq!(block.insert(0, 100), Some(10));
assert_eq!(block.insert(1, 200), Some(20));
assert_eq!(block.get(0), Some(&100));
assert_eq!(block.get(1), Some(&200));
assert_eq!(block.remove(0), Some(100));
assert_eq!(block.remove(1), Some(200));
assert!(block.is_empty());
assert_eq!(block.get(0), None);
assert_eq!(block.get(1), None);
assert_eq!(block.remove(0), None);
assert_eq!(block.remove(1), None);
动机
可空指针优化
有时,一个在栈上具有固定大小分配的直接寻址表对于简单的查找已经足够。也就是说,堆分配的 HashMap
和 Vec
可能过于复杂。直观来看,人们可能会倾向于使用 Option<T>
数组(对于某些类型 T
)来实现这样一个表。然而,这并不是理想的,因为对于大多数类型,Option<T>
(以字节为单位)的大小是不必要的。
在 Rust 中,某些类型利用了 可空指针优化。对于一些 enum
类型(如 Option
),编译器可以执行一些巧妙的技巧来最小化其内存占用。例如,考虑一个 Option<&T>
。假设在一个没有启用可空指针优化的 64 位目标上,编译器可能会天真地分配 16 字节来存储单个 Option<&T>
:8 字节用于引用(即实际的指针)加上 8 字节用于 enum
区分符。这确实是相当浪费的。
为了解决这些问题,回想一下,Rust 中的所有引用都不可能是 null。编译器可以利用这个事实,将 None::<&T>
变体分配给实际的 null 指针。因此,我们说当引用为 null 时,&T
是 None
;否则,它是 Some
变体(它有一个有效的引用)。因此,enum
区分符就不再必要了。现在,一个 Option<&T>
只需要 8 字节!
Rustonomicon 讨论了更多可以启用优化的例子。关键是:有些类型具有属性和假设,允许编译器放弃一些大小开销。 但是,如果无法进行这种大小优化怎么办?
内存占用加倍
考虑一个 Option<u64>
。函数 core::mem::size_of
告诉我们,单个 Option<u64>
占用 16 字节的内存!前 8 字节属于 u64
本身,其余 8 字节属于 enum
区分符。这同样相当浪费。
为了解决 enum
判别符开销问题,标准库提供了 core::num::NonZeroU64
类型。该 NonZeroU64
是一个无开销的包装器,用于 u64
类型,假设它不为零(正如其名称所暗示的)。
这种假设使得 NonZeroU64
有资格进行可空指针优化。也就是说,一个 Option<NonZeroU64>
如果包含 0
,则为 None
;否则,它是 Some
变体(它具有有效的非零值)。因此,我们可以删除开销,因为值已经隐式编码了判别符。现在,一个 Option<NonZeroU64>
只需 8 字节!
use core::{mem::size_of, num::NonZeroU64};
assert_eq!(size_of::<Option<u64>>(), 16);
assert_eq!(size_of::<Option<NonZeroU64>>(), 8);
因此,直接地址表,内部使用 Option<T>
值的数组,不可避免地会消耗比必要的更多内存。除非内部类型可以方便地进行可空指针优化,否则 enum
判别符开销最多会加倍内存占用。
一个新库诞生了!
然而,并非所有希望都破灭了。观察发现,Option
类型的判别符实际上可以存储为一个单独的位。因此,可以在单个位掩码中存储多个判别符(用于可选值数组)。这正是 option-block
库提供的抽象。
该库提供了五个原始类型:Block8
、Block16
、Block32
、Block64
和 Block128
。正如其名称所暗示的,一个 Block8
是最多包含 8 个可选值的块,其中内部位掩码是一个 u8
(每个单元格一个)。其余的原始类型基本上是 Block8
的 16、32、64 和 128 个元素的类似物。
use core::mem::size_of;
use option_block::Block16;
assert_eq!(size_of::<[Option<u16>; 16]>(), 64);
assert_eq!(size_of::<Block16<u16>>(), 34);
实现细节
进一步的内部细节以叙述格式在补充文章“探索不安全代码”中解释。
堆栈限制
由于 option-block
在堆栈上分配,必须小心处理 Block64
和 Block128
类型。在 Block128
类型的极端情况下,它分配 128 个内部数据类型的实例以及 16 个额外的字节用于位掩码。如果创建太多,堆栈内存使用量可能会迅速增加。因此,建议谨慎使用较大的块变体。