#枚举 #变体 # #内存 #布尔 #映射 #enum-vec

enum_like

将任何类型视为枚举的特例。如果一个类型具有相当小的变体数量,例如具有4个变体的结构体struct A(bool, bool),这个特例提供从类型值到判别值的1对1映射。它还提供所有可能值的迭代器。这个包主要是与enum_like_deriveenum_vec一起使用的。

3个不稳定版本

使用旧的Rust 2015

0.2.1 2018年11月19日
0.2.0 2018年8月2日
0.1.0 2018年7月11日

#2172Rust模式


2 crate 使用

GPL-3.0+

27KB
585

enum-vec

高效存储枚举变体向量

Crates.io License GPLv3 Build Status

文档

假设你有一个有4个变体的枚举Direction。你只需要2位来存储判别值,但Rust会使用至少1字节(8位)。因此,当你使用一个包含16个元素的Vec<Direction>时,它将使用16字节内存。然而,这个包提供了EnumVec类型,它只使用所需的位数。因此,一个包含16个元素的EnumVec<Direction>将仅使用4字节内存。

实现

由于Rust没有提供计数类型变体数的方法,因此enum_like包定义了一个具有关联常量NUM_VARIANTS的特EnumLike,并提供了一些辅助方法将usize转换为T。这个特适用于一些常见的类型,如boolOption<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]; 这样的操作甚至更差。

基准测试

以下是当存储类型需要2位存储空间时,对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等效方法快的方法是allany,它们利用打包一次性处理多个元素。某些其他基准测试看起来更快是因为重新分配:当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

另请参阅

enum-set

enum-map

enum-kinds

bit-vec

smallbitvec

smallvec


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。

无运行时依赖