zombie-rs
A Rust implementation of the Zombie monad for memory-efficient lazy evaluation with automatic eviction and recomputation.
Core Idea: Trade computation time for memory — evict values when memory is tight, recompute on demand.
Performance: 97% Memory Reduction
Scenario: 200-stage dependency chain, each stage = 80 MiB, total = 15 GiB
| Metric | Standard Rust | Zombie-rs (200 MiB Limit) |
|---|---|---|
| Peak Memory | 14.1 GiB | 402 MiB |
| Build Time | 2.1s | 0.5s |
| Access Time | 0.8s | 7.2s |
| Total Time | 8.0s | 12.7s |
Trade-off: +60% time for 97% memory reduction. Zombie computes eagerly, evicts when memory limit exceeded, replays from checkpoints on access.
How the Benchmark Works
Both benchmarks create a dependency chain where each stage depends on the previous:
stage[0] = initial data (80 MiB)
stage[i] = f(stage[i-1]) ← each stage transforms the previous
Standard Rust (bench_standard.rs):
- Must keep ALL 200 stages in memory to support random access
- Any stage access is O(1) but memory = O(n × stage_size)
Zombie-rs (bench_limited.rs):
- Sets
memory_limit = 200 MiB(only ~2-3 stages fit) - During build: computes eagerly, automatically evicts old stages
- During access: if stage was evicted, replays from nearest checkpoint
- Memory bounded regardless of chain length
# Run yourself (use /usr/bin/time -l to measure peak memory)
Quick Start
use *;
Note:
Runtime::init()initializes thread-local state. This is a Rust-specific design for thread safety.
Installation
[]
= "0.0.4"
Feature Flags:
derive(default) —#[derive(ZombieOps)]macroprofiler(default) — Memory tracking utilitiescli— Benchmark CLI (not included by default)
Core API
// Create
let z = new;
// Access (recomputes if evicted)
let val = z.get;
z.with;
// Transform
let z2 = z.map;
let z3 = z.bind;
Key Concepts
Evictable vs Non-Evictable
// NOT evictable - RootContext
let root = new;
// Evictable - created via combinators
let evictable = root.map;
Memory Limit
let config = default
.with_memory_limit; // 100MB
init_with_config;
When to Use
Based on the Zombie paper:
| Good | Poor |
|---|---|
| Large intermediate values — Images, matrices, large data | Tiny values — Overhead exceeds benefit (< 1KB) |
| Huge input — Large images, GB-sized logs, huge projects | Need all values simultaneously |
| Low memory environments — WASM (2GB limit), embedded, GPU | High-frequency random access |
| Intermediate state — Time-travel debug, Jupyter, autodiff | Side effects — Must be pure/deterministic |
| Linear dependencies — Pipelines, streaming transforms | Extremely expensive recomputation |
Benchmarking
# Note: `./benches.sh <ARGS>` is just a wrapper script for `cargo run --release --features cli -- <ARGS>`
# Quick benchmark
# Full benchmark with baseline comparison (500+ runs recommended)
# Save results as baseline
Metrics: CV (noise), Time (median), Peak (memory), Meta (overhead)
Data from zombie-rs v0.0.1; future versions may differ.
Additional Utilities (zombie_utils)
[]
= "0.0.3"
use *;
use *;
Crate Architecture
The project is organized as a Cargo workspace with the following crates:
| Crate | Description |
|---|---|
| zombie-rs | Core library - Zombie<T>, Runtime, Timeline, bind_zombie, memory management |
| zombie_derive | Proc macro #[derive(ZombieOps)] for auto-implementing size calculation on custom types |
| zombie_profiler | Zero-overhead profiling utilities: TrackingAllocator, BenchmarkSuite, formatted reports |
| zombie_utils | Ergonomic helpers: zbind! macro, bind2-bind23, combinators (sequence, zip, traverse) |
zombie_derive
The ZombieOps trait determines memory size for eviction decisions. Use #[derive(ZombieOps)] for automatic implementation:
use ZombieOps;
How it works:
stack_size()— Alwayssize_of::<Self>()(compile-time constant)heap_size()— Sum of all fields'heap_size()(generated by derive)get_size()—stack_size() + heap_size()(used by eviction algorithm)
For external types (orphan rule workaround):
;
zombie_profiler
use ;
static GLOBAL: TrackingAllocator = TrackingAllocator;
let mut suite = new;
suite.bench;
suite.bench;
suite.report; // Pretty-prints comparison table
Documentation
- examples/ — Usage examples (basic → advanced)
- tests/ — Test cases and edge cases
- paper/ — Academic paper (Typst format)
- ARCHITECTURE_REVIEW.md — Internal architecture and algorithm details
Naming & Renaming
To provide a more modern and intuitive API, we have renamed core internal components from their original Sanskrit names (used in the C++ implementation) to English descriptors:
| New Name | Original Name | Meaning |
|---|---|---|
| Runtime | Trailokya | The global state manager |
| Timeline | Akasha | The context/tock tree |
| Zombie | Zombie | Values that "die" (evict) and "resurrect" (recompute) |
References
- Zombie Paper — Original LaTeX
- C++ Implementation
License
MIT