guides Intermediate

Warm-Up and Caching Strategies

Where pyvorin-native 1.0.9 caches compiled code, what a cache hit is worth in measured milliseconds, and how to key the cache in CI.

Published Apr 24, 2026

Compilation is the one cost Pyvorin Native asks you to manage. Every compiled function pays a one-time compile — measured in milliseconds to seconds, host-dependent — and then runs at native speed indefinitely, with disk caching making repeat compiles free. Warm-up strategy is simply the discipline of paying that cost at the right moment. This page gives you the verified numbers from the installed pyvorin-native 1.0.9 build, the exact cache locations and eviction behaviour, and the cache-key scheme you should use in CI.

Two caches, two jobs

There are two caches, in two places, doing two different things — and the CLI command named cache only touches one of them.

The compile artefact cache lives inside the install tree at <site-packages>/.pyvorin_cache/disk_compile. On our test install it holds 177 index entries of SHA256-named shared objects plus a cache_index.json, each entry checksum-validated on load. The cache is an LRU capped at 500 entries: when it fills, the least recently used artefacts are evicted, and the next run of that function recompiles. This cache is the product's own and persists across processes and reboots. Because it lives inside the install tree, each virtual environment or container image carries its own compile cache — there is no shared global store, and two projects on one machine do not contend for each other's entries.

The user cache lives at ~/.pyvorin_cache and holds columnar kernel state; it is created at import time, before any licence activity — something to know if your deployment audits "no writes until activated" claims. The CLI manages this one:

python -m pyvorin cache status
Cache directory: /root/.pyvorin_cache
Total files: 158
Total size: 1.20 MB

cache clear empties that directory and leaves the compile artefact cache untouched — verified by running it. If your intention is "force everything to recompile", the CLI alone will not do it; remove the disk_compile directory as well. Both caches rebuild themselves on the next run, so clearing is always a performance decision, never a correctness one.

What a cache hit is worth

Measured on our test host, a 100,000-iteration numeric loop. First benchmark run, cold cache:

Benchmark: benchable.py (square_sum)
  warmup: 3
  runs:   5
  correct: True
  status: COMPILED_FULL
  compile_time: 259.417 ms
  min:    0.012 ms
  mean:   0.013 ms
  median: 0.012 ms

Second run of the identical file, warm cache, via bench --json: compile_time_ms: 0.0, steady-state run_time_ms: 0.0123. The compile cost did not shrink — it vanished, replaced by a checksum-verified load. That is the whole economic case for the cache in one pair of numbers: 259 milliseconds once, then zero, against a per-call cost of about 12 microseconds.

Do the amortisation arithmetic honestly and it almost always favours compiling. At those measured rates, the compile pays for itself after roughly 21,000 calls — derived by dividing the captured compile time by the captured steady-state time. A nightly batch that runs the function millions of times crosses that line in its first second. Even a web request handler at ten calls per second crosses it inside an hour of uptime. The cases that never cross it are microscopic: one-shot scripts where the loop body is so small that, as our benchmark suite shows, overhead dominates and compilation can leave you slower than the interpreter. Measure before you warm. The benchmarking workflow is the quick start's second half.

Warm-up at run time

bench defaults to 3 warm-up iterations before its timed runs; --warmup and --runs adjust both, and if you are publishing the numbers anywhere, raise the run count until the median stabilises rather than quoting a single pass. Warm-up iterations exist because the first native call after compilation pays fixed costs — artefact loading, initialisation — that steady-state calls do not, and mixing the two into one average is how benchmarking mistakes happen. The CLI keeps them apart so you cannot make that mistake by accident. run --compare runs the same warm-up and timed loop against both CPython and Pyvorin in one invocation — our capture, same workload:

CPython:  3.614 ms (result=333328333350000)
Pyvorin:  0.008 ms (result=333328333350000)
Speedup:  438.88x
Correct:  YES

Identical results, four-hundred-fold gap. Note that the compile time is deliberately excluded from that number; it is reported on its own line so amortisation is your decision, not the harness's.

For long-running services, warm-up means paying the compile before the first real request. Three mechanisms, all verified:

  • Provision-time compilation. Run python -m pyvorin compile app.py during image build or deploy. The artefact lands in the disk cache, shipped with the install tree, and the production process starts hot. This is the strongest option: the cost is paid where it hurts least.
  • Start-up precompilation, programmatically. PyvorinCompiler.precompile(source, ["fn_a", "fn_b"]) compiles a batch in one call at service start. Details on the programmatic API usage page.
  • Zero-argument entrypoints. Functions the oracle can execute validate at compile time; functions that require arguments compile unvalidated. A tiny entry() that exercises the real functions with representative data gives you validated compiles and doubles as a smoke test. The validation behaviour is documented on the error handling and diagnostics page.

Cache keys and CI

The cache key is a SHA256 hash over the function source, the function name, the compiler options and the modification times of the runtime and compiler toolchain. Three consequences fall straight out of that definition, and they drive CI design:

  • Source is the primary key. Any edit to a function invalidates its entry automatically. You never need to clear the cache to pick up a code change; the key changes for you. Clearing is for corruption or debugging, not for deploys.
  • Upgrades invalidate wholesale. Because the key includes toolchain modification times, upgrading the pyvorin-native package re-keys every artefact. A cache restored from a previous package version simply will not be consulted; nothing breaks, nothing false is reused.
  • The cache is a directory, not a database. Restoring it is a plain directory copy of <site-packages>/.pyvorin_cache/disk_compile into the matching install tree.

The CI cache key you derive from this should therefore be: package version, plus the flags you pass (--vectorize, --no-parallel, --fast-math and friends), plus a hash of the sources being compiled. Restore disk_compile before the compile step, run compile, and save the directory after. A compile that hits comes back as COMPILED_FULL with compile_time_ms: 0.0 in the JSON report — an easy assertion for your pipeline to check. In shape, the step looks like this — pseudocode, not a product command:

# restore cached artefacts into the install tree before compiling
cp -r "$CI_CACHE/disk_compile" \
      "$VENV/lib/python3.12/site-packages/.pyvorin_cache/"

python -m pyvorin compile app.py --report compile_report.json

# save the (possibly extended) cache for the next run
cp -r "$VENV/lib/python3.12/site-packages/.pyvorin_cache/disk_compile" \
      "$CI_CACHE/disk_compile"

Two cautions. Keep the cache per toolchain: an LRU capped at 500 entries will quietly evict cold entries on a busy shared cache, so a monorepo compiling thousands of functions across many branches may see recompiles even with a warm cache; partitioning by project keeps eviction local. And remember the checksum validation — a corrupted restore is detected on load and recompiled, so a bad cache costs time, never correctness.

A worked judgement call

Warm-up strategy is ultimately about where a process spends its first second. Our rule: compile everything that runs in a loop, and do it at provision time; leave one-shot and cold-path code alone even when it compiles cleanly. The temptation to precompile the entire codebase is real and mostly harmless — eviction handles the size — but it hides the useful signal. A function that only ever runs once per deploy has no business in the hot cache, and a cache full of such functions evicts the ones your latency actually depends on. Spend the warm-up budget on the request path and the batch inner loop. Everything else can afford its own first call. Cache the winners, not the catalogue.

Where to go next

Last reviewed 24 April 2026 against pyvorin-native 1.0.9 installed at /root/pvfinal. All timings on this page were captured from real runs on 13 September 2026 on a quiet host; the break-even figure is arithmetic on those captured numbers, and the cache layout was read from the installed files directly.