MemoizationKit.jl
MemoizationKit reuses the results of Julia functions called with the same arguments. It is useful when repeated computations are expensive and their caches need limits, monitoring, or persistence.
@cachedkeeps ordinary Julia method signatures, including defaults, keywords, varargs, andwhereparameters.CacheStyleselects shared, task-local, or uncached execution per function and argument type.- Built-in
ClockCacheandLRUcontainers bound storage in entries or bytes. With concrete keys and an inferred concrete return type, RAM hits do not allocate. MemoizationKit.cachekeymaps equivalent inputs to a shared result; see Custom cache keys.- Preferences set defaults per package or function; runtime tools inspect, clear, and resize global caches.
- Optional extensions add disk persistence, a terminal dashboard, and timing.
Quick start
Requires Julia 1.10 or later. Add MemoizationKit to your environment with Julia's package manager:
using Pkg
Pkg.add("MemoizationKit")using MemoizationKit
@cached function fib(n::Int)::BigInt
n < 2 ? BigInt(n) : fib(n - 1) + fib(n - 2)
end;
fib(100)354224848179261915075Calling fib(100) again reuses the stored result. By default, each function gets a shared Clock cache with room for 10,000 entries across all its cached methods.
Using MemoizationKit
Memoizing a method
Put @cached before a method definition. By default, its positional and keyword arguments form the cache key, and its body runs only on a miss.
using MemoizationKit
@cached function powers(x::T, n::Int = 3; offset::T = zero(T))::Vector{T} where {T}
[x^i + offset for i in 1:n]
end;
powers(2; offset = 1)3-element Vector{Int64}:
3
5
9The macro also supports varargs, qualified names, operators, and callable objects. Anonymous functions are not supported. Methods without @cached keep their usual behavior.
A return annotation is optional and may depend on where parameters. Without one, MemoizationKit uses the inferred return type, falling back to Any if it is not concrete. A type-stable body keeps calls inferred even though the shared cache holds results of different types.
Use uncached to call the body without reading or filling RAM or disk caches. Supply all arguments, including defaults: defaults belong to the wrapper, not the uncached body.
uncached(powers, 2, 3; offset = 1)3-element Vector{Int64}:
3
5
9Choosing a strategy
Define CacheStyle(f, args...) for the function and positional argument types you want to customize. A strategy based on types can be resolved by the compiler.
| Strategy | Use it when |
|---|---|
GlobalCache() | Tasks should share results (the default). |
GlobalCache{LRU}() | You want exact least-recently-used eviction. |
TaskLocalCache() | Tasks should keep separate caches, reducing contention on shared storage. |
NoCache() | Computing is cheaper than caching, or results should not be reused. |
For example, give each task its own cache for a recursive computation:
using MemoizationKit
@cached fib(n::Int)::BigInt = n < 2 ? BigInt(n) : fib(n - 1) + fib(n - 2);
MemoizationKit.CacheStyle(::typeof(fib), ::Int) = TaskLocalCache{LRU}();
fib(10)55To skip caching floating-point calls to powers:
MemoizationKit.CacheStyle(::typeof(powers), ::AbstractFloat, ::Int) = NoCache();
powers(2.0; offset = 1.0)3-element Vector{Float64}:
3.0
5.0
9.0GlobalCache() and TaskLocalCache() use the configured container, Clock by default. See Eviction policies for how Clock and LRU choose entries to remove. GlobalLRUCache() is an alias for GlobalCache{LRU}().
Task-local caches live for the task's lifetime and are absent from the global management API and dashboard. Built-in task-local containers are bounded, but TaskLocalCache{Dict}() is unbounded. Task-local caches can be separate per key and value type; their limits are not a single budget for the whole function.
DiskCacheStyle selects disk caching independently. NoCache() as the RAM strategy can still read and fill a disk cache; see Disk caching.
Inspecting and limiting caches
Global caches are created on first use. Normally, one cache holds all cached methods and argument types of a function. If its strategy selects several container types, each gets a separate cache with the function's size limit.
using MemoizationKit
module MyPackage
using MemoizationKit
@cached powers(x) = [x^i for i in 1:3]
end
MyPackage.powers(2)
cache_info(MyPackage) # functions owned by this module and submodules1-element Vector{Pair{Any, MemoizationKit.AbstractCache}}:
Main.MyPackage.powers => ClockCache{Any, Any}(1/10000 entries, 0 hits, 1 misses)Resize a function's cache and inspect it after inserting three keys:
set_cache_size!(MyPackage.powers, 2) # count entries
foreach(MyPackage.powers, 1:3)
cache_info(MyPackage.powers)1-element Vector{Pair{Any, MemoizationKit.AbstractCache}}:
Main.MyPackage.powers => ClockCache{Any, Any}(2/2 entries, 1 hits, 3 misses)Switch to a byte limit, or clear a module's caches:
set_cache_size!(MyPackage.powers, 2^20; by = MemoizationKit.cachesize)
MyPackage.powers(2)
empty_caches!(MyPackage) # keep hit/miss counters
cache_info(MyPackage)1-element Vector{Pair{Any, MemoizationKit.AbstractCache}}:
Main.MyPackage.powers => ClockCache{Any, Any}(0 entries, size 0/1048576, 0 hits, 1 misses)cache_info() and empty_caches!() act on all global caches.
Shrinking a cache evicts entries; changing its size measure discards it. A value larger than the limit is returned but not retained by the built-in containers. Byte limits measure values, not keys or container overhead, and are not a process-wide memory budget. See Configuration for persistent defaults and custom size measures.
Keys, results, and invalidation
- Default keys use
hashandisequal, with an additional type check:f(3)andf(3.0)are different entries. Keyword values are included; forwarded keyword order can also distinguish keys. - Keep keys unchanged while cached. Mutating an array used as a key can invalidate its hash.
- RAM results are shared objects. Copy a mutable result before changing it.
- Use caching for computations determined by their arguments. Changes to external state or method definitions (including through Revise) do not automatically invalidate results. Clear the global cache with
empty_caches!; for disk results, updateMemoizationKit.diskversion. - Built-in shared caches are thread-safe. Computations run outside the cache lock, allowing recursion; simultaneous misses may compute the same key more than once. Exceptions are propagated without storing a result.
Use MemoizationKit.cachekey to map equivalent inputs to a common key, or Hashed to customize hashing and equality. See Custom cache keys for examples and the requirements for sharing results.
Why MemoizationKit?
MemoizationKit is for memoization that needs ongoing management: bounded RAM caches, reuse across runs, and tools to inspect and adjust caches while a program runs. Strategies, settings, persistence, and monitoring work together under one function-level API.
| Package | Choose it when | Tradeoff |
|---|---|---|
| Memoize.jl | You want a small @memoize API with a dictionary of your choice. | Limits and statistics depend on the container; persistence, a dashboard, and package settings require additional integration. |
| Memoization.jl | You need to memoize individual calls or closures, as well as method definitions. | Its default cache is unbounded and not thread-safe; custom containers can address those needs, but management and persistence tools require additional integration. |
| LRUCache.jl | You want a thread-safe cache dictionary with limits, statistics, resizing, and eviction callbacks. | You write the function wrapper and management code, or combine it with a memoization macro. |
| MemoizationKit.jl | You want bounded caches, per-type strategies, disk persistence, and live monitoring under one API. | The macro requires a method definition; shared caches use locks, and disk results need explicit versioning and cleanup. |
Memoize.jl and Memoization.jl both accept LRUCache.jl containers, so bounded memory and statistics are available without MemoizationKit. Memoization.jl also preserves return-type inference. For a single in-memory cache, these packages may already cover your needs.
Key matching: Memoize.jl and Memoization.jl default to identity-based IdDict caches and allow other dictionaries. MemoizationKit defaults to value-based matching with distinct entries for different argument types; custom keys can merge equivalent inputs. Hashing large keys can cost more than recomputing.
Invalidation: MemoizationKit requires manual clearing after code or external state changes. Memoization.jl handles method redefinition for memoized definitions, though memoized individual calls require clearing; see its limitations. Those limitations also describe thread safety for top-level functions with a thread-safe container, but not closures or callable objects.
Disk caching adds serialization and file I/O, so it suits expensive computations. Disk storage has no automatic eviction, and RAM limits are per cache rather than process-wide. See Disk caching and Configuration for these controls.