Skip to content
rusthacks.dev
Performance

Caching Computed Values in Rust With Memoization Explained

Cache the result of an expensive pure function in a HashMap or with the cached crate. Includes the recursion pattern, key design, bounded size and when memoizing is the wrong fix.

3 min read
Summarize in AI
Flat illustration for the post “Caching Computed Values in Rust With Memoization Explained”, composed around cargo.

Computing the same expensive thing twice is the easiest performance bug to fix, and the easiest to fix wrongly. Memoization stores the result keyed by the input. It works for pure functions, which is the condition people forget. This post shows the plain HashMap version, the recursive one, and the limits. For caches that outlive one computation, see in-process caches. Both belong to the performance cluster started in profiling Rust programs.

Rust 1.90, edition 2024.

A cache in front of a function

rust
use std::collections::HashMap;

fn slow_square(n: u64) -> u64 {
    // pretend this is expensive
    n * n
}

struct Memo {
    cache: HashMap<u64, u64>,
}

impl Memo {
    fn new() -> Self { Memo { cache: HashMap::new() } }

    fn square(&mut self, n: u64) -> u64 {
        if let Some(&v) = self.cache.get(&n) {
            return v;
        }
        let v = slow_square(n);
        self.cache.insert(n, v);
        v
    }
}

A second call with the same n returns from the map. With the entry API the two lookups collapse into one: *self.cache.entry(n).or_insert_with(|| slow_square(n)), covered in the HashMap entry API.

Recursion

The classic case is Fibonacci, where the naive recursion repeats work exponentially.

rust
use std::collections::HashMap;

fn fib(n: u64, memo: &mut HashMap<u64, u64>) -> u64 {
    if n < 2 {
        return n;
    }
    if let Some(&v) = memo.get(&n) {
        return v;
    }
    let v = fib(n - 1, memo) + fib(n - 2, memo);
    memo.insert(n, v);
    v
}

fn main() {
    let mut memo = HashMap::new();
    println!("{}", fib(50, &mut memo));
}

It prints 12586269025. The naive version makes roughly forty billion calls for fib(50); the memoized one makes about a hundred. For this particular function a loop with two variables needs no map at all, so memoization is a tool for functions where the repeated subproblems are not so tidy.

Key design

The key must include everything the result depends on. A function price(product, currency) cached by product alone returns euros to a dollar customer. Reading an environment variable or the clock inside a "memoized" function makes it impure and the cache wrong. Floating-point keys are awkward, because f64 is not Hash; use the bit pattern (to_bits) or round to an integer.

Bound it

Each distinct input adds an entry. If inputs are user supplied, the cache grows without limit and an attacker can fill your memory. Use a maximum entry count with eviction (LRU), or a time to live. The moka crate provides both with thread-safe access, and cached offers an attribute macro (#[cached]) that memoizes a function in one line.

Threads

A Mutex<HashMap> held while computing serializes every caller. Lock, check, unlock, compute, lock, insert; accept that two threads may compute the same value at once, or use moka's get_with, which makes concurrent callers for one key wait for a single computation.

When not to memoize

Cheap functions, inputs that never repeat, and results that depend on mutable state. Check the hit rate: count hits and misses, and remove the cache if misses dominate. Cache invalidation, always the hard part, is discussed in the performance posts around in-process caches. The standard library's HashMap entry API is the building block.

Taggedcache
Share

Keep reading

Flat illustration for the post “Stale While Revalidate Caching in Rust Services Explained”, composed around perf.
Performance

Stale While Revalidate Caching in Rust Services Explained

Serve the cached value immediately, even if slightly old, and refresh it in the background. Learn the idea, the HTTP Cache-Control directives and an in-process implementation with a freshness window and a grace window.

4 min read

Get new posts by email

We send one email when a new post is published. No spam.

Unsubscribe in one click. No spam, ever.