title: “Testing and Benchmarking HashMap in Rust” meta_description: “Comprehensive testing strategies, benchmarks, and Miri validation for HashMap implementations in Rust.” keywords: [“rust hashmap testing”, “miri hashmap”, “benchmark hashmap”, “property testing”, “criterion benchmarks”]
🧪 Reinventing from Scratch: Building HashMap
Chapter 8 — “Tests, Benchmarks, and Miri”
“If you haven’t run it under Miri, you haven’t tested it.”
🧭 8.1. Testing Strategy
A robust HashMap needs multiple layers of testing:
- Unit tests — Individual operations
- Integration tests — Combined operations
- Property tests — Invariant checking
- Benchmarks — Performance validation
- Miri — Undefined behavior detection
✅ 8.2. Unit Tests
Test: Basic Insert and Get
#[test]
fn test_insert_and_get() {
let mut map = MyHashMap::new();
map.insert("apple", 1);
map.insert("banana", 2);
assert_eq!(map.get("apple"), Some(&1));
assert_eq!(map.get("banana"), Some(&2));
assert_eq!(map.get("cherry"), None);
}
Test: Update Existing Key
#[test]
fn test_insert_updates_existing() {
let mut map = MyHashMap::new();
assert_eq!(map.insert("key", 1), None);
assert_eq!(map.insert("key", 2), Some(1)); // Returns old value
assert_eq!(map.get("key"), Some(&2));
}
Test: Remove
#[test]
fn test_remove() {
let mut map = MyHashMap::new();
map.insert("a", 1);
map.insert("b", 2);
assert_eq!(map.remove("a"), Some(1));
assert_eq!(map.remove("a"), None); // Already removed
assert_eq!(map.get("b"), Some(&2)); // Other entries intact
}
🔄 8.3. Collision and Probe Tests
Test: Forced Collisions
#[test]
fn test_handles_collisions() {
use std::hash::{BuildHasherDefault, Hasher};
// Custom hasher that returns predictable values
struct PredictableHasher(u64);
impl Hasher for PredictableHasher {
fn finish(&self) -> u64 { self.0 }
fn write(&mut self, _: &[u8]) { }
fn write_u64(&mut self, i: u64) { self.0 = i; }
}
struct PredictableBuilder;
impl BuildHasher for PredictableBuilder {
type Hasher = PredictableHasher;
fn build_hasher(&self) -> Self::Hasher {
PredictableHasher(0)
}
}
let mut map = MyHashMap::with_hasher(PredictableBuilder);
// All these will hash to the same bucket!
map.insert(0, "zero");
map.insert(16, "sixteen"); // 16 % 16 = 0
map.insert(32, "thirty-two"); // 32 % 16 = 0
// All should still be retrievable
assert_eq!(map.get(&0), Some(&"zero"));
assert_eq!(map.get(&16), Some(&"sixteen"));
assert_eq!(map.get(&32), Some(&"thirty-two"));
}
📈 8.4. Resize and Growth Tests
Test: Automatic Growth
#[test]
fn test_grows_automatically() {
let mut map = MyHashMap::with_capacity(4);
map.insert(1, "a");
map.insert(2, "b");
map.insert(3, "c");
assert!(map.capacity() == 4);
map.insert(4, "d"); // Should trigger growth (load > 0.75)
assert!(map.capacity() > 4);
// All entries still findable
assert_eq!(map.get(&1), Some(&"a"));
assert_eq!(map.get(&4), Some(&"d"));
}
🎲 8.5. Property-Based Testing
Using proptest or quickcheck:
use proptest::prelude::*;
proptest! {
#[test]
fn test_insert_then_get_returns_value(key: String, value: i32) {
let mut map = MyHashMap::new();
map.insert(key.clone(), value);
assert_eq!(map.get(&key), Some(&value));
}
#[test]
fn test_len_matches_actual_count(ops: Vec<(String, i32)>) {
let mut map = MyHashMap::new();
for (k, v) in ops {
map.insert(k, v);
}
let actual_count = map.iter().count();
assert_eq!(map.len(), actual_count);
}
#[test]
fn test_remove_then_get_returns_none(
inserts: Vec<(String, i32)>,
remove_keys: Vec<String>
) {
let mut map = MyHashMap::new();
for (k, v) in inserts {
map.insert(k, v);
}
for key in &remove_keys {
map.remove(key);
}
for key in &remove_keys {
assert_eq!(map.get(key), None);
}
}
}
⚡ 8.6. Benchmarking with Criterion
use criterion::{black_box, criterion_group, criterion_main, Criterion};
fn bench_insert(c: &mut Criterion) {
c.bench_function("insert 1000", |b| {
b.iter(|| {
let mut map = MyHashMap::new();
for i in 0..1000 {
map.insert(black_box(i), black_box(i * 2));
}
});
});
}
fn bench_get(c: &mut Criterion) {
let mut map = MyHashMap::new();
for i in 0..10000 {
map.insert(i, i * 2);
}
c.bench_function("get from 10k", |b| {
b.iter(|| {
for i in 0..1000 {
black_box(map.get(&black_box(i)));
}
});
});
}
fn bench_iterate(c: &mut Criterion) {
let mut map = MyHashMap::new();
for i in 0..10000 {
map.insert(i, i);
}
c.bench_function("iterate 10k", |b| {
b.iter(|| {
for (k, v) in &map {
black_box(k);
black_box(v);
}
});
});
}
criterion_group!(benches, bench_insert, bench_get, bench_iterate);
criterion_main!(benches);
🔬 8.7. Miri: The Ultimate Test
Miri detects undefined behavior:
cargo +nightly miri test
What Miri Catches
- Use-after-free
- Uninitialized memory reads
- Invalid pointer arithmetic
- Data races (even in single-threaded code via interior mutability)
- Alignment violations
Example Test Under Miri
#[test]
fn test_miri_safe_insert_remove() {
let mut map = MyHashMap::new();
// Fill map
for i in 0..100 {
map.insert(i, format!("value_{}", i));
}
// Remove half
for i in (0..100).step_by(2) {
map.remove(&i);
}
// Verify remaining
for i in (1..100).step_by(2) {
assert!(map.get(&i).is_some());
}
// Drop map (Miri checks Drop implementation)
}
If there’s any UB (wrong pointer arithmetic, etc.), Miri will catch it!
📊 8.8. Performance Comparison
Compare your HashMap against std:
fn bench_comparison(c: &mut Criterion) {
let mut group = c.benchmark_group("hashmap_comparison");
group.bench_function("std::HashMap insert", |b| {
b.iter(|| {
let mut map = std::collections::HashMap::new();
for i in 0..1000 {
map.insert(i, i);
}
});
});
group.bench_function("MyHashMap insert", |b| {
b.iter(|| {
let mut map = MyHashMap::new();
for i in 0..1000 {
map.insert(i, i);
}
});
});
group.finish();
}
Expected: Your HashMap will be 2-10x slower (that’s OK! Learning > performance).
🎯 8.9. Load Factor Analysis
Measure how probe distance increases with load:
#[test]
fn measure_probe_distances() {
let mut map = MyHashMap::with_capacity(1000);
let mut max_distance = 0;
for i in 0..750 { // Fill to 75%
map.insert(i, i);
// Check max distance
for (idx, entry) in map.entries.iter().enumerate() {
if let Occupied(e) = entry {
let dist = map.probe_distance(e.hash, idx);
max_distance = max_distance.max(dist);
}
}
}
println!("Max probe distance at 75% load: {}", max_distance);
assert!(max_distance < 20, "Probe distances too high!");
}
🧠 8.10. Chapter Takeaways
| Concept | Purpose |
|---|---|
| Unit tests | Test individual operations |
| Property tests | Verify invariants hold |
| Criterion | Accurate benchmarking |
| Miri | Catch undefined behavior |
| Load factor analysis | Measure performance degradation |
🚀 8.11. What’s Next
We’ve built a complete, tested HashMap!
Final chapter: Appendix — Implementation Skeleton
Get the complete reference implementation with all the pieces assembled.
💡 Exercises
- Write 20+ unit tests covering all operations
- Add property tests for insert/remove/get invariants
- Benchmark against std::HashMap
- Run under Miri and fix any issues
- Measure probe distance distribution across different load factors
- Test with complex types (String, Vec, custom structs)
Next: Chapter 9 — Appendix: Implementation Skeleton 📝