Skip to content

Commit ea0c3b5

Browse files
committed
feat: const-sized stack-allocated enummap
Heap allocation isn't implicit, it is rather left for the downstream crate to decide by boxing the enum.
1 parent 0f1239c commit ea0c3b5

File tree

10 files changed

+494
-548
lines changed

10 files changed

+494
-548
lines changed

README.md

Lines changed: 121 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -9,99 +9,150 @@ Enum-centric data structures for Rust.
99

1010
## Usage
1111

12-
### EnumMap
12+
A map of enum variants to values. EnumMap is a fixed-size map, where each variant of the enum
13+
is mapped to a value. This implementation EnumMap is a a zero-cost abstraction over an array (const-sized), where the index of the array corresponds to the position of the variant in the enum.
1314

14-
EnumMap is a special case of a Hash Map, with better **computational complexity** guarantees and overall **performance**. Can differentiante between a missing (`Option::None`)
15-
and set (`Option::Some`) value.
15+
Because it is a thin wrapper over an array, it is stack-allocated by default. Simply [std::boxed::Box]ing it will move it to the heap, at the caller's discretion.
1616

17-
Abstracts away the need to handle [Option] on insert/remove operations.
18-
It marginally is faster to initialize than `EnumTable`, because `Default` value needn't be cloned for each field.
17+
- Indexed by enum variants.
18+
- IndexMut by enum variants.
19+
- Debug if the enum is Debug.
20+
- PartialEq if the value is PartialEq. Same for Eq.
1921

20-
Using `get` and `insert` functions.
22+
Debug and Eq are optional features. They are enabled by default.
2123

2224
```rust
23-
use enum_collections::{EnumMap, Enumerated};
24-
#[derive(Enumerated)]
25-
enum Letter {
26-
A,
27-
B,
28-
}
29-
assert_eq!(Letter::VARIANTS.len(), 2); // VARIANTS provided by this crate
30-
31-
let mut map: EnumMap<Letter, u8> = EnumMap::new();
32-
map.insert(Letter::A, 42);
33-
assert_eq!(Some(&42u8), map.get(Letter::A));
34-
map.remove(Letter::A);
35-
assert_eq!(None, map.get(Letter::A));
36-
```
37-
38-
Using `Index` and `IndexMut` syntactic sugar.
39-
```rust
40-
use enum_collections::{EnumMap, Enumerated};
41-
#[derive(Enumerated)]
42-
enum Letter {
43-
A,
44-
B,
45-
}
25+
use enum_collections::{EnumMap, Enumerated};
26+
27+
#[derive(Enumerated)]
28+
pub enum Letter {
29+
A,
30+
B,
31+
}
32+
33+
// Indexing and mutation
34+
let mut enum_map = EnumMap::<Letter, i32, { Letter::SIZE }>::new_default();
35+
assert_eq!(0, enum_map[Letter::A]);
36+
enum_map[Letter::A] = 42;
37+
assert_eq!(42, enum_map[Letter::A]);
38+
39+
// Constructor with default values
40+
let enum_map_default = EnumMap::<Letter, i32, { Letter::SIZE }>::new_default();
41+
assert_eq!(0, enum_map_default[Letter::A]);
42+
assert_eq!(0, enum_map_default[Letter::B]);
43+
44+
// Convenience constructor for optional values
45+
let mut enum_map_option = EnumMap::<Letter, Option<i32>, { Letter::SIZE }>::new_option();
46+
assert_eq!(None, enum_map_option[Letter::A]);
47+
assert_eq!(None, enum_map_option[Letter::B]);
48+
enum_map_option[Letter::A] = Some(42);
49+
assert_eq!(Some(42), enum_map_option[Letter::A]);
50+
51+
// Constructor with custom initialization
52+
#[derive(PartialEq, Eq, Debug)]
53+
struct Custom;
54+
let enum_map = EnumMap::<Letter, Custom, { Letter::SIZE }>::new(|| Custom);
55+
assert_eq!(Custom, enum_map[Letter::A]);
56+
assert_eq!(Custom, enum_map[Letter::B]);
57+
58+
// Custom initialization function with enum variant (key) inspection
59+
let enum_map = EnumMap::<Letter, i32, { Letter::SIZE }>::new_inspect(|letter| {
60+
match letter {
61+
Letter::A => 42,
62+
Letter::B => 24,
63+
}
64+
});
65+
assert_eq!(42, enum_map[Letter::A]);
66+
assert_eq!(24, enum_map[Letter::B]);
67+
68+
// Debug
69+
#[derive(Enumerated, Debug)]
70+
pub enum LetterDebugDerived {
71+
A,
72+
B,
73+
}
74+
let enum_map_debug =
75+
EnumMap::<LetterDebugDerived, i32, { LetterDebugDerived::SIZE }>::new(|| 42);
76+
assert_eq!("{A: 42, B: 42}", format!("{:?}", enum_map_debug));
4677

47-
let mut map: EnumMap<Letter, u8> = EnumMap::new();
48-
map[Letter::A] = Some(42);
49-
assert_eq!(Some(42u8), map[Letter::A]);
50-
assert_eq!(Some(&42u8), map[Letter::A].as_ref());
51-
```
78+
```
5279

53-
### EnumTable
5480

55-
EnumTable is a special case of a Hash Map, with better **computational complexity** guarantees and overall **performance**. Initialized with default values, can NOT differentiate between missing values
56-
and values actually set.
81+
Iterate over enum variants
5782

58-
Using Index and IndexMut syntactic sugar.
5983

6084
```rust
61-
use enum_collections::{EnumTable, Enumerated};
62-
#[derive(Enumerated)]
63-
enum Letter {
64-
A,
65-
B,
66-
}
67-
assert_eq!(Letter::VARIANTS.len(), 2); // VARIANTS provided by this crate
68-
69-
let mut map: EnumTable<Letter, u8> = EnumTable::new();
70-
map[Letter::A] = 42;
71-
assert_eq!(42u8, map[Letter::A]);
72-
assert_eq!(u8::default(), map[Letter::B]);
85+
#[derive(Enumerated, Debug)]
86+
pub enum Letter {
87+
A,
88+
B,
89+
}
90+
91+
Letter::VARIANTS
92+
.iter()
93+
.for_each(|letter| println!("{:?}", letter));
7394
```
7495

96+
7597
## Features
7698

7799
Portions of functionality are feature-flagged, but enabled by default. This is to allow turning this functionality off when not needed, e.g. `Debug` and `Eq` implementations.
78-
79100
See [docs.rs](https://docs.rs/crate/enum-collections/latest/features) for details.
80101

81102
## Benchmarks
82103

83-
There are single-threaded benchmarks for the `get`, `insert` and `remove` operations in [enum-collections/benches](enum-collections/benches/). Invoke `cargo bench` to run them.
104+
Invoke `cargo bench` to run benchmarks. While `EnumMap` operates in pico-seconds, `std::collections::HashMap` in > 10 nanoseconds.
84105

85-
### EnumMap
86-
```
87-
NAME lower bound | est | upper bound
88-
EnumMap get time: [635.02 ps 635.52 ps 636.06 ps] est ~22x faster
89-
std::collections::HashMap get time: [13.971 ns 13.986 ns 14.002 ns]
90-
91-
EnumMap insert time: [890.74 ps 892.13 ps 893.66 ps] est ~15,57x faster
92-
std::collections::HashMap insert time: [13.889 ns 13.895 ns 13.901 ns]
106+
<details>
107+
<summary>Benchmark results</summary>
93108

94-
EnumMap remove time: [481.07 ps 481.79 ps 482.53 ps] est ~28,55x faster
95-
std::collections::HashMap remove time: [13.704 ns 13.737 ns 13.771 ns]
96109
```
97-
98-
### EnumTable
110+
EnumMap get time: [221.09 ps 221.59 ps 222.21 ps]
111+
Found 10 outliers among 100 measurements (10.00%)
112+
5 (5.00%) high mild
113+
5 (5.00%) high severe
114+
115+
EnumMap insert time: [230.05 ps 233.38 ps 236.25 ps]
116+
Found 2 outliers among 100 measurements (2.00%)
117+
1 (1.00%) high mild
118+
1 (1.00%) high severe
119+
120+
EnumMap new: default time: [852.31 ps 853.28 ps 854.37 ps]
121+
Found 2 outliers among 100 measurements (2.00%)
122+
1 (1.00%) low mild
123+
1 (1.00%) high mild
124+
125+
EnumMap new: Option::None
126+
time: [1.7100 ns 1.7110 ns 1.7120 ns]
127+
Found 2 outliers among 100 measurements (2.00%)
128+
1 (1.00%) high mild
129+
1 (1.00%) high severe
130+
131+
EnumMap new: provider fn
132+
time: [791.17 ps 792.38 ps 793.65 ps]
133+
Found 7 outliers among 100 measurements (7.00%)
134+
1 (1.00%) low mild
135+
4 (4.00%) high mild
136+
2 (2.00%) high severe
137+
138+
EnumMap new: inspecting provider fn
139+
time: [775.03 ps 776.84 ps 778.92 ps]
140+
Found 8 outliers among 100 measurements (8.00%)
141+
4 (4.00%) high mild
142+
4 (4.00%) high severe
143+
144+
std::collections::HashMap get
145+
time: [13.433 ns 13.484 ns 13.543 ns]
146+
Found 8 outliers among 100 measurements (8.00%)
147+
3 (3.00%) high mild
148+
5 (5.00%) high severe
149+
150+
std::collections::HashMap insert
151+
time: [14.094 ns 14.107 ns 14.121 ns]
152+
Found 4 outliers among 100 measurements (4.00%)
153+
1 (1.00%) high mild
154+
3 (3.00%) high severe
99155
100156
```
101-
NAME lower bound | est | upper bound
102-
EnumTable Index get time: [460.22 ps 460.81 ps 461.41 ps] est ~1.113x faster
103-
Crate Enum-Map Index get time: [512.16 ps 512.62 ps 513.13 ps]
104157

105-
EnumTable insert time: [670.83 ps 671.43 ps 672.06 ps] est. ~1,06x faster
106-
Crate Enum-Map insert time: [715.68 ps 716.34 ps 717.04 ps]
107-
```
158+
</details>

enum-collections-macros/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ documentation = "https://docs.rs/enum-collections-macros"
1616
[lib]
1717
proc-macro = true
1818

19+
[features]
20+
default = []
21+
variants = []
22+
1923
[dependencies]
2024
syn = "1.0"
2125
quote = "1.0"

enum-collections-macros/src/lib.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ pub fn derive_enum_collections(input: TokenStream) -> TokenStream {
1616
.into();
1717
};
1818

19+
let enum_len = en.variants.len();
1920
let mut variants = proc_macro2::TokenStream::new();
20-
2121
for variant in en.variants {
2222
if let Some((_, discriminant)) = variant.discriminant {
2323
return quote_spanned! {
@@ -36,6 +36,8 @@ pub fn derive_enum_collections(input: TokenStream) -> TokenStream {
3636
self as usize
3737
}
3838

39+
const SIZE: usize = #enum_len;
40+
#[cfg(feature = "variants")]
3941
const VARIANTS: &'static [Self] = &[#variants];
4042
}
4143
}

enum-collections/Cargo.toml

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,20 +14,16 @@ documentation = "https://docs.rs/enum-collections"
1414

1515
[features]
1616
default = ["debug", "eq"]
17-
debug = []
17+
debug = ["variants"] # Array of all variants must be available at runtime to generate keys in the Debug output
1818
eq = []
19+
variants = ["enum-collections-macros/variants"]
1920

2021
[dependencies]
2122
enum-collections-macros = { path = "../enum-collections-macros", version = "0.3.0" }
2223

2324
[dev-dependencies]
24-
criterion = "0.4.0"
25-
enum-map = "2.4.2" # Benchmark
25+
criterion = "0.5.1"
2626

2727
[[bench]]
2828
name = "enummap"
2929
harness = false
30-
31-
[[bench]]
32-
name = "enumtable"
33-
harness = false

enum-collections/benches/enummap.rs

Lines changed: 41 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
1-
extern crate enum_map;
21
use std::{collections::HashMap, hash::Hash};
32

43
use criterion::{black_box, criterion_group, criterion_main, Criterion};
54
use enum_collections::{EnumMap, Enumerated};
6-
use enum_map::Enum;
75

8-
#[derive(Eq, PartialEq, Hash, Enum, Enumerated)] // Enum derived to benchmark against the `enum-map` crate
6+
#[derive(Enumerated, Eq, PartialEq, Hash)] // Enum derived to benchmark against the `enum-map` crate
97
#[allow(dead_code)]
108
enum Letter {
119
A,
@@ -18,63 +16,74 @@ enum Letter {
1816
}
1917

2018
fn enummap_get(criterion: &mut Criterion) {
21-
let mut enum_map: EnumMap<Letter, u8> = EnumMap::new();
22-
enum_map.insert(Letter::A, 1);
19+
let mut enum_map: EnumMap<Letter, i32, { Letter::SIZE }> = EnumMap::new_default();
20+
enum_map[Letter::A] = 1;
2321
criterion.bench_function("EnumMap get", |bencher| {
24-
bencher.iter(|| enum_map.get(black_box(Letter::A)))
22+
bencher.iter(|| enum_map[Letter::A]) // Tested without a black box, expected to be optimized in real-world usage
2523
});
2624
}
2725

2826
fn enummap_insert(criterion: &mut Criterion) {
29-
let mut enum_map: EnumMap<Letter, u8> = EnumMap::new();
27+
let mut enum_map: EnumMap<Letter, i32, { Letter::SIZE }> = EnumMap::new_default();
28+
enum_map[Letter::A] = 1;
3029
criterion.bench_function("EnumMap insert", |bencher| {
31-
bencher.iter(|| enum_map.insert(black_box(Letter::A), black_box(1)))
30+
bencher.iter(|| enum_map[Letter::A] = black_box(42))
3231
});
3332
}
3433

35-
fn enummap_remove(criterion: &mut Criterion) {
36-
let mut enum_map: EnumMap<Letter, u8> = EnumMap::new();
37-
criterion.bench_function("EnumMap remove", |bencher| {
38-
bencher.iter(|| enum_map.remove(black_box(Letter::A)))
34+
fn enummap_new_default(criterion: &mut Criterion) {
35+
criterion.bench_function("EnumMap new: default", |bencher| {
36+
bencher.iter(|| EnumMap::<Letter, i32, { Letter::SIZE }>::new_default())
3937
});
4038
}
4139

42-
fn std_hashmap_get(criterion: &mut Criterion) {
43-
let mut hashmap: HashMap<Letter, u8> = HashMap::new();
44-
hashmap.insert(Letter::A, 1);
45-
criterion.bench_function("std::collections::HashMap get", |bencher| {
46-
bencher.iter(|| hashmap.get(black_box(&Letter::A)))
40+
fn enummap_new_option(criterion: &mut Criterion) {
41+
criterion.bench_function("EnumMap new: Option::None", |bencher| {
42+
bencher.iter(|| EnumMap::<Letter, Option<i32>, { Letter::SIZE }>::new_option())
4743
});
4844
}
4945

50-
fn std_hashmap_insert(criterion: &mut Criterion) {
51-
let mut hashmap: HashMap<Letter, u8> = HashMap::new();
52-
criterion.bench_function("std::collections::HashMap insert", |bencher| {
53-
bencher.iter(|| hashmap.insert(black_box(Letter::A), black_box(1)))
46+
fn enummap_new(criterion: &mut Criterion) {
47+
criterion.bench_function("EnumMap new: provider fn", |bencher| {
48+
bencher.iter(|| (EnumMap::<Letter, i32, { Letter::SIZE }>::new(|| 42))) // Tested without a black box, expected to be optimized in real-world usage
5449
});
5550
}
5651

57-
fn std_hashmap_remove(criterion: &mut Criterion) {
58-
let mut hashmap: HashMap<Letter, u8> = HashMap::new();
59-
criterion.bench_function("std::collections::HashMap remove", |bencher| {
60-
bencher.iter(|| hashmap.remove(black_box(&Letter::A)))
52+
fn enummap_new_inspect(criterion: &mut Criterion) {
53+
criterion.bench_function("EnumMap new: inspecting provider fn", |bencher| {
54+
bencher.iter(|| {
55+
EnumMap::<Letter, i32, { Letter::SIZE }>::new_inspect(|variant| match variant {
56+
Letter::A => 1,
57+
_ => 42,
58+
})
59+
})
6160
});
6261
}
6362

64-
fn enummap_init(criterion: &mut Criterion) {
65-
criterion.bench_function("EnumMap init", |bencher| {
66-
bencher.iter(|| black_box(EnumMap::<Letter, u8>::new()))
63+
fn std_hashmap_get(criterion: &mut Criterion) {
64+
let mut hashmap: HashMap<Letter, i32> = HashMap::new();
65+
hashmap.insert(Letter::A, 1);
66+
criterion.bench_function("std::collections::HashMap get", |bencher| {
67+
bencher.iter(|| hashmap.get(&Letter::A))
68+
});
69+
}
70+
71+
fn std_hashmap_insert(criterion: &mut Criterion) {
72+
let mut hashmap: HashMap<Letter, i32> = HashMap::new();
73+
criterion.bench_function("std::collections::HashMap insert", |bencher| {
74+
bencher.iter(|| hashmap.insert(Letter::A, 1))
6775
});
6876
}
6977

7078
criterion_group!(
7179
benches,
7280
enummap_get,
73-
std_hashmap_get,
7481
enummap_insert,
82+
enummap_new_default,
83+
enummap_new_option,
84+
enummap_new,
85+
enummap_new_inspect,
86+
std_hashmap_get,
7587
std_hashmap_insert,
76-
enummap_remove,
77-
std_hashmap_remove,
78-
enummap_init,
7988
);
8089
criterion_main!(benches);

0 commit comments

Comments
 (0)