3个不稳定版本
使用旧的Rust 2015
0.2.1 | 2018年11月19日 |
---|---|
0.2.0 | 2018年8月2日 |
0.1.0 | 2018年7月11日 |
#2172 在 Rust模式
被 2 crate 使用
27KB
585 行
enum-vec
高效存储枚举变体向量
假设你有一个有4个变体的枚举Direction
。你只需要2位来存储判别值,但Rust会使用至少1字节(8位)。因此,当你使用一个包含16个元素的Vec<Direction>
时,它将使用16字节内存。然而,这个包提供了EnumVec
类型,它只使用所需的位数。因此,一个包含16个元素的EnumVec<Direction>
将仅使用4字节内存。
实现
由于Rust没有提供计数类型变体数的方法,因此enum_like
包定义了一个具有关联常量NUM_VARIANTS
的特EnumLike
,并提供了一些辅助方法将usize
转换为T
。这个特适用于一些常见的类型,如bool
和Option<T>
,并且可以为任何类型实现。可以使用enum_like_derive
包来自动实现,该包提供了一个#[derive(EnumLike)]
过程宏。
示例
在您的Cargo.toml
中添加以下内容:
[dependencies]
enum_vec = "0.3"
enum_like = "0.2"
enum_like_derive = "0.1"
然后在src/main.rs
中:
#[macro_use]
extern crate enum_like_derive;
extern crate enum_like;
extern crate enum_vec;
use enum_vec::EnumVec;
#[derive(Copy, Clone, Debug, EnumLike)]
enum Direction {
Left, Right, Up, Down,
}
fn main() {
let mut v = EnumVec::new();
v.push(Direction::Left);
v.push(Direction::Right);
v.push(Direction::Left);
v.push(Direction::Right);
for d in v {
println!("{:?}", d);
}
}
有关更多使用示例,请参阅examples/src/main.rs
。
BitVec
由于EnumVec本质上是一个n位vec,因此您可以将其用作这样。
type BitVec = EnumVec<bool>;
type TwoBitVec = EnumVec<[bool; 2]>;
type TwoBitVec = EnumVec<(bool, bool)>;
type FourBitVec = EnumVec<[bool; 4]>;
派生EnumLike
您可以为几乎所有类型自动派生EnumLike
,只要其所有字段都是EnumLike
。
struct BitField {
opt_0: bool,
opt_1: bool,
opt_2: bool,
opt_3: bool,
}
enum BitsOrRaw {
Bits(BitField),
Raw { opt_01: (bool, bool), opt_23: (bool, bool), },
}
实现EnumLike
您可以为EnumLike
编写自定义实现:以下代码允许创建一个EnumVec<Digit>
,其中每个元素是4位,而不是由u8
要求的8位。
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
struct Digit {
x: u8, // x >= 0 && x <= 9
}
unsafe impl EnumLike for Digit {
const NUM_VARIANTS: usize = 10;
fn to_discr(self) -> usize {
self.x as usize
}
fn from_discr(x: usize) -> Self {
let x = x as u8;
Self { x }
}
}
由于其他代码假定to_discr
永远不会返回大于NUM_VARIANTS
的值,因此此特是不安全的。
内存效率
由于默认情况下每个块是32位,因此当每个元素为1、2、4、8、16或32位长时,EnumVec
才达到100%的内存效率。这是因为元素永远不会跨两个块分割:存储在32位块内的15位元素将始终使用30位,并浪费剩下的2位。通常,效率可以计算为1 - (32 % n) / 32
,但总是等于或优于正常的Vec
。但是,当n >= 11时,它等于,因此如果您有一个有2048个变体的类型,您应该考虑使用Vec
。
n | Vec | EnumVec8 | EnumVec16 | EnumVec32 | EnumVec64 | EnumVec128 |
---|---|---|---|---|---|---|
1 | 0.125 | 1 | 1 | 1 | 1 | 1 |
2 | 0.25 | 1 | 1 | 1 | 1 | 1 |
3 | 0.375 | 0.75 | 0.9375 | 0.9375 | 0.984375 | 0.984375 |
4 | 0.5 | 1 | 1 | 1 | 1 | 1 |
5 | 0.625 | 0.625 | 0.9375 | 0.9375 | 0.9375 | 0.9765625 |
6 | 0.75 | 0.75 | 0.75 | 0.9375 | 0.9375 | 0.984375 |
7 | 0.875 | 0.875 | 0.875 | 0.875 | 0.984375 | 0.984375 |
8 | 1 | 1 | 1 | 1 | 1 | 1 |
9 | 0.5625 | 0 | 0.5625 | 0.84375 | 0.984375 | 0.984375 |
10 | 0.625 | 0 | 0.625 | 0.9375 | 0.9375 | 0.9375 |
11 | 0.6875 | 0 | 0.6875 | 0.6875 | 0.859375 | 0.9453125 |
完整的表格作为Python单行脚本提供
x = [(n, n/8 if n <= 8 else n/16 if n <= 16 else n/32 if n <= 32 else n/64, 1-(8%n)/8, 1-(16%n)/16, 1-(32%n)/32, 1-(64%n)/64, 1-(128%n)/128) for n in range(1, 64+1)]
一个EnumVec8
具有8位存储块无法用于存储大于8位的项。类似地,对于存储大于32位的元素,默认的EnumVec32
是不够的。一个项目在位上的最大大小在EnumLike
包中定义为可以放入一个usize
中的位数量。具有128位存储的EnumVec
是目前最内存高效的选项,但在典型64位机器上,大多数操作的速度比其他实现慢2倍。8、16、32和64位版本的性能相似。
每个 EnumVecN
的 "效率限制",即比 Vec
更优的最大项大小(以位为单位),如下所示:
存储大小 | 效率限制 |
---|---|
EnumVec8 | 4 |
EnumVec16 | 4 |
EnumVec32 | 11 |
EnumVec64 | 22 |
EnumVec128 | 42 |
自定义
要更改默认存储,请从内部模块导入 EnumVec
use enum_vec::vec_u64::EnumVec;
use enum_vec::vec_u8::EnumVec as EnumVec8;
这将使 EnumVec
使用 64 位块,提高内存效率,并添加使用 8 位块的 EnumVec8
的选项。注意,enum_vec![]
宏始终创建一个 EnumVec
,所以以下代码将无法编译:
let a: EnumVec8 = enum_vec![];
将无法编译。
应选择哪种存储大小?
- 使用
EnumVec8
以最小化小型向量的开销,实际上可以考虑使用SmallEnumVec
。 - 对于非常大的向量,尤其是当元素位大小不是 2 的幂时,请使用
EnumVec64
,因为它在某些情况下更节省内存。 - 只有当内存效率比性能更重要时,才使用
EnumVec128
。 - 如果性能比内存效率更重要,请使用
Vec
。 - 如果大多数时候需要存储少量元素(多达 128 位),请使用
SmallEnumVec
。
PackedU8
当项目大小为 8 或 16 位时,使用 Vec
始终是更好的选择。但这并不总是容易,因为 Vec<[bool; 8]>
将使用每个元素 8 字节而不是 8 位。要强制它使用 8 位,请将其包装为 Vec<PackedU8<[bool; 8]>>
use enum_like::PackedU8;
let a = vec![PackedU8::new([true; 8]); 10];
for x in a {
let x = x.value();
}
SmallEnumVec
在以下位置有一个实验性的 SmallEnumVec
可用:
use enum_vec::smallvec_u32::EnumVec as SmallEnumVec;
当使用 smallvec
功能编译时,该功能在 Cargo.toml
中启用
enum_vec = { version = "0.3", features = ["smallvec"] }
当编译时,SmallEnumVec
将使用堆栈来存储项目,并且仅在它变得太大时才分配。默认情况下,现在使用 4x32 位的内联存储。这将允许存储 128 个 1 位项目、64 个 2 位、32 个 4 位等。
有关更多信息,请参阅 smallvec crate。
缺点
- 由于
EnumVec
无法返回引用,因此没有索引语法。请使用 get 和 set 代替。 - 您不能使用切片方法,如 split()、get(range)、reverse()、chunk 和 window 迭代器、sort()、dedup() 等。因为没有实现 deref (与
&Vec
相似,后者可以用作&[T]
). - 大多数操作(push、pop、insert、remove)的速度比
Vec
相应操作慢 2 或 3 倍。像 extend、from_slice 或vec![None; 1000];
这样的操作甚至更差。
基准测试
以下是当存储类型Vec<T>
与EnumVec<T>
的比较。
(提交 e8db9c883b82e472e9aefb6087be55dafd76b6a0)
name normal_vec2 ns/iter enum_vec32_2 ns/iter diff ns/iter diff % speedup
::bench_all 3 5 2 66.67% x 0.60
::bench_all_small 3 5 2 66.67% x 0.60
::bench_all_worst_case 1,308 41 -1,267 -96.87% x 31.90
::bench_all_worst_case_small 19 5 -14 -73.68% x 3.80
::bench_any 8 12 4 50.00% x 0.67
::bench_any_small 8 12 4 50.00% x 0.67
::bench_any_worst_case 447 59 -388 -86.80% x 7.58
::bench_any_worst_case_small 11 6 -5 -45.45% x 1.83
::bench_extend 419 3,793 3,374 805.25% x 0.11
::bench_extend_small 48 108 60 125.00% x 0.44
::bench_from_slice 180 3,237 3,057 1698.33% x 0.06
::bench_from_slice_small 27 79 52 192.59% x 0.34
::bench_insert 8,059 13,154 5,095 63.22% x 0.61
::bench_insert_at_zero 16,898 38,729 21,831 129.19% x 0.44
::bench_insert_at_zero_small 218 190 -28 -12.84% x 1.15
::bench_insert_small 275 258 -17 -6.18% x 1.07
::bench_iter_all 2,327 4,948 2,621 112.63% x 0.47
::bench_macro_from_elem 602 2,435 1,833 304.49% x 0.25
::bench_macro_from_elem_small 28 80 52 185.71% x 0.35
::bench_push 4,914 7,097 2,183 44.42% x 0.69
::bench_push_small 181 130 -51 -28.18% x 1.39
::bench_pushpop 4,390 12,107 7,717 175.79% x 0.36
::bench_remove 5,261 10,823 5,562 105.72% x 0.49
::bench_remove_at_zero 15,880 68,593 52,713 331.95% x 0.23
::bench_remove_at_zero_small 101 443 342 338.61% x 0.23
::bench_remove_small 103 207 104 100.97% x 0.50
唯一肯定比Vec
等效方法快的方法是all
和any
,它们利用打包一次性处理多个元素。某些其他基准测试看起来更快是因为重新分配:当Vec
达到1、2、4、8、...个元素时将进行重新分配,而EnumVec
将每32/n、64/n、...进行一次重新分配,并且由于基准测试中n=2,"_small"基准测试中的插入次数默认为16,因此Vec
将重新分配4次,而EnumVec
将重新分配1次。
要运行基准测试,请下载源代码并运行
cargo +nightly bench --features smallvec > bench_log
cargo benchcmp normal_vec2 enum_vec32_2 bench_log
您需要安装cargo-benchcmp才能轻松比较基准测试。例如,要比较默认的32位EnumVec
与8位的EnumVec
,在处理4位元素时运行
cargo benchcmp enum_vec32_4 enum_vec8_4 bench bench_log
另请参阅
lib.rs
:
此crate提供了EnumLike
特质,它定义了一个从给定类型到usize
的映射。
这类似于std::mem::discriminant
,但是有一些区别。首先,所有值都是从零开始的连续的。这意味着如果一个枚举有10个变体,其判别符将始终小于10。如果一个字段有显式的判别符,则该值将被忽略:在enum { A = 100 }
中,A
的值将是0。最重要的是,此特质允许从usize
创建类型的实例,因为如果存在枚举数据,则判别符(如果可能)也编码在判别符中。例如
enum ABC { A, B, C }
enum DEF { D, E, F }
enum AD { A(ABC), D(DEF) }
AD
枚举有2个变体,但是由于每个变体都是3个变体的枚举,所以AD::values().count()
将返回6而不是2。