Skip to content

perf: reduce per-instruction overhead in VM execution hot paths - #2391

Open
orizi wants to merge 5 commits into
mainfrom
claude/vm-execution-performance-32hi0c
Open

orizi wants to merge 5 commits into
mainfrom
claude/vm-execution-performance-32hi0c

Conversation

@orizi

@orizi orizi commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Reduces per-instruction overhead in the VM execution hot paths:

  • Memory::get: skip the relocation round-trip and redundant 40-byte copy for values that cannot be affected by relocation rules (everything except pointers into temporary segments). Hits 3–4 reads per instruction.
  • Memory::insert: compute segment indexes once instead of twice, and compare MemoryCells directly (ignoring the ACCESS flag) instead of materializing a MaybeRelocatable for the consistency check.
  • Range-check validation: validate inline instead of via a boxed closure, removing a heap allocation per range-checked value.
  • step_instruction: drop the per-step mem::take + resize of the instruction cache; copy the (Copy) Instruction out instead. Also keeps the cache intact if run_instruction errors.
  • deduce_op1: take op0 by reference, avoiding a felt clone per deduced operand.
  • update_registers: take operands by reference instead of by value (~168-byte move per step).
  • BuiltinHintProcessor: skip hashing the full hint source on every hint execution when no extra hints are registered.
  • mark_as_accessed: batch the four per-step address marks, resolving each segment once for consecutive same-segment addresses.

Measured on cairo_programs/benchmarks (best-of-5, --layout all_cairo --proof_mode): −10% to −32% wall clock on most benchmarks (big_fibonacci −32%, pedersen −28%, linear_search −20%, keccak_integration −18%).

🤖 Generated with Claude Code

Performance

Expected: removes per-instruction overhead on every VM step — 3–4 relocation round-trips + 40-byte copies per step in Memory::get, a heap alloc per range-checked value, a mem::take+resize per step in the instruction cache, a felt clone per deduced operand, a ~168-byte operands move per step, a full-source hash per hint execution, and redundant index computations in insert/mark_as_accessed. Applies to essentially every workload; biggest where steps are cheap (tight loops) so per-step overhead dominates.

Measured (best-of-7, wall clock vs main, local; noise ±2.4%):

benchmark delta benchmark delta
poseidon_integration −20.2% keccak_integration −14.5%
pedersen −20.4% big_factorial −13.2%
linear_search −20.7% memory_integration −13.2%
big_fibonacci −16.3% math_integration −9.3%
integration_builtins −14.0% secp_integration −7.7%
uint256_integration −14.0% dict_integration −7.3%

Every benchmark improves, −7% to −21%. An independent earlier measurement on a different machine (4-core container, best-of-5, before the insert/mark_as_accessed commits were added) showed the same shape: big_fibonacci −32%, pedersen −28%, linear_search −20%, keccak −18%, dict −16%, big_factorial −13%, math −10%.

Methodology: cairo_programs/benchmarks/*.cairo compiled with cairo-lang 0.14 --proof_mode, run via cairo-vm-cli <prog>.json --layout all_cairo --proof_mode, hyperfine best-of-7 per binary, local x86-64 Linux. Noise floor (identical-code control pair): ±2.4%.


This change is Reviewable

claude and others added 2 commits September 9, 2026 10:54
- Memory::get: skip the relocation round-trip and redundant copy for
  values that cannot be affected by relocation rules (everything except
  pointers into temporary segments).
- Memory::insert: compute segment indexes once instead of twice.
- Range-check validation: replace the boxed-closure rule with an inline
  tagged rule, removing a heap allocation per range-checked value.
- step_instruction: drop the per-step mem::take + resize of the
  instruction cache; copy the (Copy) Instruction out instead. This also
  keeps the cache intact if run_instruction returns an error.
- deduce_op1: take op0 by reference, avoiding a felt clone per deduced
  operand.
- update_registers: take operands by reference instead of by value.
- BuiltinHintProcessor: skip hashing the full hint source on every hint
  execution when no extra hints are registered.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cp3KwJwrs4QKjhpMLeAA3f
…ssed

- Memory::insert: convert the incoming value to a MemoryCell once and
  compare cells directly (ignoring the ACCESS flag), instead of
  materializing a MaybeRelocatable from the stored cell for the
  consistency check on every write.
- mark_as_accessed_batch: mark the four per-step addresses (dst, op0,
  op1, pc) in one call, resolving each segment once for consecutive
  same-segment addresses.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown
**Hyper Thereading Benchmark results**




hyperfine -r 2 -n "hyper_threading_main threads: 1" 'RAYON_NUM_THREADS=1 ./hyper_threading_main' -n "hyper_threading_pr threads: 1" 'RAYON_NUM_THREADS=1 ./hyper_threading_pr'
Benchmark 1: hyper_threading_main threads: 1
  Time (mean ± σ):     24.669 s ±  0.013 s    [User: 24.210 s, System: 0.456 s]
  Range (min … max):   24.660 s … 24.679 s    2 runs
 
Benchmark 2: hyper_threading_pr threads: 1
  Time (mean ± σ):     20.630 s ±  0.021 s    [User: 20.191 s, System: 0.436 s]
  Range (min … max):   20.616 s … 20.645 s    2 runs
 
Summary
  hyper_threading_pr threads: 1 ran
    1.20 ± 0.00 times faster than hyper_threading_main threads: 1




hyperfine -r 2 -n "hyper_threading_main threads: 2" 'RAYON_NUM_THREADS=2 ./hyper_threading_main' -n "hyper_threading_pr threads: 2" 'RAYON_NUM_THREADS=2 ./hyper_threading_pr'
Benchmark 1: hyper_threading_main threads: 2
  Time (mean ± σ):     13.204 s ±  0.037 s    [User: 24.266 s, System: 0.456 s]
  Range (min … max):   13.178 s … 13.230 s    2 runs
 
Benchmark 2: hyper_threading_pr threads: 2
  Time (mean ± σ):     10.890 s ±  0.011 s    [User: 20.471 s, System: 0.442 s]
  Range (min … max):   10.883 s … 10.898 s    2 runs
 
Summary
  hyper_threading_pr threads: 2 ran
    1.21 ± 0.00 times faster than hyper_threading_main threads: 2




hyperfine -r 2 -n "hyper_threading_main threads: 4" 'RAYON_NUM_THREADS=4 ./hyper_threading_main' -n "hyper_threading_pr threads: 4" 'RAYON_NUM_THREADS=4 ./hyper_threading_pr'
Benchmark 1: hyper_threading_main threads: 4
  Time (mean ± σ):      9.778 s ±  0.303 s    [User: 35.794 s, System: 0.586 s]
  Range (min … max):    9.563 s …  9.992 s    2 runs
 
Benchmark 2: hyper_threading_pr threads: 4
  Time (mean ± σ):      8.703 s ±  0.153 s    [User: 31.436 s, System: 0.581 s]
  Range (min … max):    8.594 s …  8.811 s    2 runs
 
Summary
  hyper_threading_pr threads: 4 ran
    1.12 ± 0.04 times faster than hyper_threading_main threads: 4




hyperfine -r 2 -n "hyper_threading_main threads: 6" 'RAYON_NUM_THREADS=6 ./hyper_threading_main' -n "hyper_threading_pr threads: 6" 'RAYON_NUM_THREADS=6 ./hyper_threading_pr'
Benchmark 1: hyper_threading_main threads: 6
  Time (mean ± σ):      9.804 s ±  0.030 s    [User: 35.865 s, System: 0.610 s]
  Range (min … max):    9.783 s …  9.825 s    2 runs
 
Benchmark 2: hyper_threading_pr threads: 6
  Time (mean ± σ):      8.436 s ±  0.050 s    [User: 31.544 s, System: 0.607 s]
  Range (min … max):    8.401 s …  8.471 s    2 runs
 
Summary
  hyper_threading_pr threads: 6 ran
    1.16 ± 0.01 times faster than hyper_threading_main threads: 6




hyperfine -r 2 -n "hyper_threading_main threads: 8" 'RAYON_NUM_THREADS=8 ./hyper_threading_main' -n "hyper_threading_pr threads: 8" 'RAYON_NUM_THREADS=8 ./hyper_threading_pr'
Benchmark 1: hyper_threading_main threads: 8
  Time (mean ± σ):      9.519 s ±  0.067 s    [User: 36.542 s, System: 0.619 s]
  Range (min … max):    9.472 s …  9.567 s    2 runs
 
Benchmark 2: hyper_threading_pr threads: 8
  Time (mean ± σ):      8.399 s ±  0.046 s    [User: 31.990 s, System: 0.648 s]
  Range (min … max):    8.367 s …  8.432 s    2 runs
 
Summary
  hyper_threading_pr threads: 8 ran
    1.13 ± 0.01 times faster than hyper_threading_main threads: 8




hyperfine -r 2 -n "hyper_threading_main threads: 16" 'RAYON_NUM_THREADS=16 ./hyper_threading_main' -n "hyper_threading_pr threads: 16" 'RAYON_NUM_THREADS=16 ./hyper_threading_pr'
Benchmark 1: hyper_threading_main threads: 16
  Time (mean ± σ):      9.731 s ±  0.224 s    [User: 36.684 s, System: 0.700 s]
  Range (min … max):    9.572 s …  9.889 s    2 runs
 
Benchmark 2: hyper_threading_pr threads: 16
  Time (mean ± σ):      8.602 s ±  0.216 s    [User: 32.120 s, System: 0.684 s]
  Range (min … max):    8.449 s …  8.754 s    2 runs
 
Summary
  hyper_threading_pr threads: 16 ran
    1.13 ± 0.04 times faster than hyper_threading_main threads: 16


@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown

Benchmark Results for unmodified programs 🚀

Command Mean [s] Min [s] Max [s] Relative
base big_factorial 1.977 ± 0.039 1.929 2.066 1.07 ± 0.02
head big_factorial 1.843 ± 0.010 1.826 1.858 1.00
Command Mean [s] Min [s] Max [s] Relative
base big_fibonacci 1.905 ± 0.022 1.878 1.946 1.08 ± 0.02
head big_fibonacci 1.765 ± 0.021 1.741 1.803 1.00
Command Mean [s] Min [s] Max [s] Relative
base blake2s_integration_benchmark 6.243 ± 0.051 6.150 6.299 1.16 ± 0.04
head blake2s_integration_benchmark 5.373 ± 0.187 5.193 5.827 1.00
Command Mean [s] Min [s] Max [s] Relative
base compare_arrays_200000 2.013 ± 0.027 1.980 2.060 1.08 ± 0.03
head compare_arrays_200000 1.862 ± 0.053 1.828 1.995 1.00
Command Mean [s] Min [s] Max [s] Relative
base dict_integration_benchmark 1.303 ± 0.034 1.266 1.363 1.05 ± 0.04
head dict_integration_benchmark 1.241 ± 0.035 1.215 1.324 1.00
Command Mean [ms] Min [ms] Max [ms] Relative
base field_arithmetic_get_square_benchmark 980.2 ± 21.6 959.4 1036.5 1.12 ± 0.04
head field_arithmetic_get_square_benchmark 876.3 ± 20.4 859.8 920.8 1.00
Command Mean [s] Min [s] Max [s] Relative
base integration_builtins 6.262 ± 0.035 6.203 6.327 1.16 ± 0.04
head integration_builtins 5.409 ± 0.203 5.283 5.952 1.00
Command Mean [s] Min [s] Max [s] Relative
base keccak_integration_benchmark 6.485 ± 0.162 6.319 6.880 1.19 ± 0.03
head keccak_integration_benchmark 5.463 ± 0.073 5.394 5.595 1.00
Command Mean [s] Min [s] Max [s] Relative
base linear_search 2.045 ± 0.067 1.985 2.216 1.09 ± 0.04
head linear_search 1.868 ± 0.041 1.833 1.970 1.00
Command Mean [s] Min [s] Max [s] Relative
base math_cmp_and_pow_integration_benchmark 1.443 ± 0.024 1.422 1.504 1.04 ± 0.03
head math_cmp_and_pow_integration_benchmark 1.391 ± 0.028 1.364 1.455 1.00
Command Mean [s] Min [s] Max [s] Relative
base math_integration_benchmark 1.358 ± 0.031 1.324 1.418 1.05 ± 0.03
head math_integration_benchmark 1.291 ± 0.022 1.269 1.347 1.00
Command Mean [s] Min [s] Max [s] Relative
base memory_integration_benchmark 1.166 ± 0.021 1.142 1.199 1.05 ± 0.03
head memory_integration_benchmark 1.115 ± 0.019 1.090 1.140 1.00
Command Mean [s] Min [s] Max [s] Relative
base operations_with_data_structures_benchmarks 1.364 ± 0.013 1.342 1.389 1.04 ± 0.02
head operations_with_data_structures_benchmarks 1.314 ± 0.028 1.292 1.362 1.00
Command Mean [ms] Min [ms] Max [ms] Relative
base pedersen 426.5 ± 17.1 412.3 457.2 1.08 ± 0.05
head pedersen 395.6 ± 7.3 389.7 412.9 1.00
Command Mean [ms] Min [ms] Max [ms] Relative
base poseidon_integration_benchmark 520.7 ± 5.2 515.9 533.9 1.00 ± 0.04
head poseidon_integration_benchmark 519.3 ± 20.0 506.9 562.6 1.00
Command Mean [s] Min [s] Max [s] Relative
base secp_integration_benchmark 1.467 ± 0.055 1.401 1.576 1.09 ± 0.05
head secp_integration_benchmark 1.345 ± 0.029 1.322 1.413 1.00
Command Mean [ms] Min [ms] Max [ms] Relative
base set_integration_benchmark 522.1 ± 14.7 511.0 558.6 1.07 ± 0.04
head set_integration_benchmark 487.1 ± 8.6 482.4 511.2 1.00
Command Mean [s] Min [s] Max [s] Relative
base uint256_integration_benchmark 3.715 ± 0.056 3.634 3.848 1.13 ± 0.04
head uint256_integration_benchmark 3.288 ± 0.099 3.221 3.544 1.00

orizi and others added 2 commits September 9, 2026 12:47
- Pin the one remaining tag-referenced actions/cache/restore to the v3
  commit SHA (org policy rejects tag-pinned actions).
- Run cargo-machete directly: the bnjbvr/cargo-machete action internally
  uses clechasseur/rs-cargo by tag, which the policy rejects transitively.
- Move the install canary from debian:11 (EOL, expired Release files) to
  debian:12.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit 05f9326)
Under Node 24, the v3 cache/restore action logs the fail-on-cache-miss
error but exits 0, so a cache miss silently produces jobs running with
incomplete program caches (and merge-caches then persists a poisoned
all-programs entry in the PR's cache scope). v4 fails properly, and its
entries use a different cache version, which also side-steps
already-poisoned v3 entries.

iai_pr/iai_main/fuzzer still use v3: bumping the iai pair requires
repopulating the main-scoped baseline caches first, or PR iai runs will
hard-fail on the (previously silent) baseline miss.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit 5666249)
@codecov

codecov Bot commented Sep 9, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 99.35897% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 96.23%. Comparing base (eeebcc9) to head (c610d41).

Files with missing lines Patch % Lines
vm/src/vm/vm_memory/memory.rs 99.06% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2391      +/-   ##
==========================================
+ Coverage   96.21%   96.23%   +0.01%     
==========================================
  Files         107      107              
  Lines       37925    37985      +60     
==========================================
+ Hits        36490    36555      +65     
+ Misses       1435     1430       -5     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

vm/src/tests/cairo_test_suite has no programs on main, so its build
target produces nothing, no cache is ever saved, and the restore in
merge-caches can never hit (previously masked by the v3 exit-0 bug).
Skip the restore while the suite is empty; once programs land, hashFiles
is non-empty and the step hard-fails on real misses again.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit 666b691)

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants