varmap
A Rust library for heterogeneous, typed key–value maps where each key holds exactly one value of a known type at a time. Values are stored in a compact 16-byte tagged representation; strings, byte slices, and large integers use an internal arena so reads return borrowed references (&str, &[u8]) without per-lookup allocation.
Three map types share the same value encoding and getter API, but differ in how keys are represented:
| Map | Key type | Best when |
|---|---|---|
VarMap |
Precomputed Key (64-bit hash) |
Keys are known at compile time (var! macro) |
StrVarMap |
&str (FNV-1a hash at runtime) |
Keys come from user input or config at runtime |
EnumVarMap |
Enum variant (#[derive(EnumVarMap)]) |
Fixed, closed set of keys known at compile time |
Usage model
VarMap is aimed at write once, read many workloads: you populate keys (configuration, request context, parsed attributes) and then read them repeatedly with cheap typed getters and borrowed &str / &[u8] results.
The internal arena is append-only. Calling set on an existing key updates the map entry, but any previous arena allocation for that key is not reclaimed. Each overwrite of an arena-backed value (strings and byte slices longer than 14 bytes, i128 / u128, Ipv6Addr, custom types, etc.) adds new arena space while the old payload remains until the whole map is reset. Scalars and short strings (≤14 bytes) live entirely in the fixed 16-byte cell, so overwriting those does not grow the arena.
| Pattern | Suitability |
|---|---|
| Set each key once, then many reads | Ideal |
| Occasional updates to a few keys | Fine |
| Frequent updates to the same keys with large / arena-backed values | Poor — arena memory grows; prefer clear() and repopulate, or another map |
| Hot loop re-assigning every key every tick | Poor — use clear() between logical snapshots, or avoid varmap |
Use clear() when you need a fresh binding of all keys (for example between requests or benchmark iterations). That resets the arena and removes current entries while preserving already allocated capacity for future writes.
Installation
Add to your Cargo.toml:
[]
= "0.1"
The proc-macros (var!, #[derive(EnumVarMap)], #[derive(VarMapValue)]) are re-exported from the varmap crate.
Supported value types
Built-in types (via set / get / typed getters):
- Integers:
i8…i64,u8…u64,i128,u128 - Floats:
f32,f64 bool,char&str,&[u8](stored in the arena; returned as borrows)IpAddr,Ipv4Addr,Ipv6Addr
Strings and byte slices up to 14 bytes are stored inline in the map; larger payloads are arena-allocated.
Custom Copy types with alignment 1–16 can use #[derive(VarMapValue)] (see Custom types).
StrVarMap
Use when variable names are ordinary strings at runtime (configuration keys, script variables, etc.). Keys are hashed with FNV-1a on each set / get.
use StrVarMap;
let mut map = new;
map.set;
map.set;
map.set;
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
// Generic get when the type is inferred or specified explicitly
let port: u16 = map.get.unwrap;
Typed getters
All three maps provide the same convenience methods:
get_bool, get_u8, get_u16, get_u32, get_u64, get_i8, get_i16, get_i32, get_i64, get_f32, get_f64, get_str, get_bytes, get_char, get_ip, get_ipv4, get_ipv6
Or use the generic API:
map.;
map.;
In-place updates
All three maps (VarMap, StrVarMap, and EnumVarMap) support in-place mutation through update.
This is useful for counters and custom Copy values that implement VarMapValue::update (the derive macro generates this automatically).
use ;
let mut map = new;
map.set;
map.set;
assert!;
assert!;
assert_eq!;
assert_eq!;
update returns false if the key is missing, the stored type does not match, or the type cannot be updated in place.
Other operations
map.clear; // drop all keys and reset the arena
VarMap
Use when key names are fixed at compile time. The var! macro expands to a Key holding the FNV-1a hash of the string literal, so lookups skip string hashing and comparison.
use ;
let mut map = new;
// Compile-time keys (recommended)
map.set;
map.set;
assert_eq!;
assert_eq!;
assert!;
assert_eq!;
// Or supply a precomputed hash yourself
map.set;
StrVarMap is a thin wrapper around VarMap that hashes &str keys at runtime; VarMap + var! is the faster option when names are static.
Limits
VarMap (and therefore StrVarMap) supports at most 65 536 distinct keys (index stored in the low 16 bits of the internal hash entry).
EnumVarMap
Use when the key set is a closed enum known at compile time. Each variant maps to a fixed slot (one per enum discriminant), giving O(1) access with no hashing or binary search.
Derive EnumVarMap on your enum (requires #[repr(u16)], Copy, and at most 65 536 variants):
use EnumVarMap;
let mut map = new;
map.set;
map.set;
map.set;
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
EnumVarMap owns its own arena (unlike StrVarMap, which delegates to an inner VarMap). Memory for every enum variant slot is reserved up front, including variants that have not been set yet.
Custom types
Types that are Copy, have alignment between 1 and 16 bytes, and contain only plain data can derive VarMapValue:
use ;
let mut map = new;
let p = Point ;
map.set;
let back: &Point = map.get.unwrap;
assert_eq!;
#[derive(VarMapValue)] also generates in-place update support, so custom values can be mutated with map.update::<T>(...) on all three map types.
Generics, unions, and non-Copy types are not supported by the derive macro.
Choosing a map
compile-time keys?
│
┌────────────┴────────────┐
yes no
│ │
fixed enum? StrVarMap
│
┌────────┴────────┐
yes no
│ │
EnumVarMap VarMap + var!
EnumVarMap— fastest reads/writes when the key set is an enum; pays fixed storage for every variant slot.VarMap+var!— same value layer asStrVarMap, but no per-operation string hashing.StrVarMap— simplest API when keys are dynamic strings.
Performance
Benchmarks live in the banches crate. They reflect the intended read-heavy pattern: each run performs 10 000 outer iterations over 256 string keys. Update tests call clear() at the start of every iteration, then write all keys once (so arena growth from repeated overwrites is not measured). Read tests populate the map once, then only read in the timed loop — matching write-once, read-many usage.
Results below are averaged over repeated runs; columns are time, allocation during init, and allocation during the timed loop (via a tracking allocator).
| Implementation | Scenario | Time | Init alloc | Run alloc |
|---|---|---|---|---|
| EnumVarMap | Update – large strings | 23 ms | 4 112 B | 16 384 B |
| VarMap | Update – large strings | 74 ms | 0 B | 28 672 B |
| StrVarMap | Update – large strings | 90 ms | 0 B | 29 696 B |
| HashMap | Update – large strings | 175 ms | 0 B | 30 641 B |
| BTreeMap | Update – large strings | 258 ms | 0 B | 26 985 B |
| EnumVarMap | Update – small strings | 31 ms | 4 112 B | 0 B |
| VarMap | Update – small strings | 75 ms | 0 B | 12 288 B |
| StrVarMap | Update – small strings | 92 ms | 0 B | 12 288 B |
| HashMap | Update – small strings | 174 ms | 0 B | 22 026 B |
| BTreeMap | Update – small strings | 255 ms | 0 B | 18 370 B |
| EnumVarMap | Read – large strings | 16 ms | 20 496 B | 0 B |
| VarMap | Read – large strings | 59 ms | 28 672 B | 0 B |
| HashMap | Read – large strings | 58 ms | 30 641 B | 0 B |
| StrVarMap | Read – large strings | 85 ms | 28 672 B | 0 B |
| BTreeMap | Read – large strings | 93 ms | 26 985 B | 0 B |
| EnumVarMap | Read – small strings | 10 ms | 4 112 B | 0 B |
| VarMap | Read – small strings | 35 ms | 12 288 B | 0 B |
| HashMap | Read – small strings | 52 ms | 22 026 B | 0 B |
| StrVarMap | Read – small strings | 54 ms | 12 288 B | 0 B |
| BTreeMap | Read – small strings | 90 ms | 18 370 B | 0 B |
Large values are long descriptive strings (~50+ bytes); small values are short tokens (a few bytes), many of which use inline storage (≤14 bytes) instead of the arena.
Takeaways
- Read benchmarks are the sweet spot (populate once, read many):
EnumVarMapleads by a wide margin (e.g. 10 ms vs 35–54 ms for small strings on read). EnumVarMapis fastest overall: direct indexing avoids hash tables and, forVarMap/StrVarMap, sorted hash-vector lookup.VarMapbeatsStrVarMapby roughly 15–20% becausevar!removes runtime FNV-1a over key names.- All three varmap types beat
HashMapandBTreeMapon the measured workloads. Update numbers assumeclear()per iteration; sustained in-place overwrites withoutclear()would allocate more because of the append-only arena. EnumVarMaptrades memory for speed: ~4 KiB upfront per 256-key enum (empty slots), but zero run-time allocation on small-string read/update tests once initialized.- Inline small strings (≤14 bytes) avoid arena use:
EnumVarMapreports 0 bytes allocated during small-string update loops.
Reproduce locally from the workspace root:
Use cargo run -p banches -- LIST to see individual benchmark names and filter with an optional fourth argument (e.g. enumvarmap).
License
MIT — see repository and crate metadata for details.