Home · ASPLOS 2026 · Compilers & Programming Languages

Evaluating Compiler Optimization Impacts on zkVM Performance

Thomas Gassmann, Stefanos Chaliasos, Thodoris Sotiropoulos and Zhendong Su

Conference
ASPLOS 2026
Published
2026
Pages
697-714
Citations
2

A zero-knowledge virtual machine lets you write a program in Rust or C++, compile it to RISC-V like normal, and then produce a cryptographic proof that it ran correctly — but the compiler doing the optimizing was built for real CPUs, so it chases things a zkVM does not have, like caches and branch prediction, while ignoring what actually costs a zkVM (every instruction becomes proof constraints); this paper (ETH Zurich and collaborators) is the first systematic study of how standard compiler optimizations affect zkVMs, finds they help far less than on a CPU and sometimes backfire, identifies what really drives the cost, and shows that a few zkVM-aware tweaks to LLVM — under 100 lines — speed some programs up by as much as 45 percent.

The argument in one glance

  1. Problem

    A zkVM runs your code to prove it ran, but its compiler was built for a different machine

    A zero-knowledge virtual machine lets a developer write an ordinary program in Rust or C++, compile it to a standard RISC-V binary, and then generate a cryptographic proof that the program executed correctly — without anyone having to hand-write the arithmetic circuits that zero-knowledge proofs used to require. That is a huge convenience, and it works by reusing the whole conventional toolchain, LLVM included. But there is a mismatch buried in that reuse. LLVM optimizes code for a real CPU, chasing goals like fitting data in caches, predicting branches, and exposing instruction-level parallelism. A zkVM has none of those things: it executes strictly in order inside an arithmetic circuit, and its real cost is that every single instruction executed becomes a set of constraints that must be proved. So the optimizer is spending its effort on features that do not exist here, and no one had systematically checked whether its transformations actually help — or hurt — the thing that matters, which is proof generation.

  2. What people did

    Just reuse the standard compiler and its optimization levels

    The pragmatic default is to compile zkVM programs with the same LLVM, at the same familiar optimization levels (like the highest, dash-O-three), that everyone uses for CPUs, trusting that decades of compiler engineering will make the code fast here too.

  3. Why it fell short

    The compiler optimizes for hardware the zkVM does not have

    Standard optimizations make their decisions using cost models built for CPU features — assuming a division is far costlier than a few bitwise operations, that memory access is cheap when cached, that eliminating a branch avoids a misprediction penalty. In a zkVM those assumptions are wrong: instructions have roughly uniform cost, so trading one division for several bitwise operations adds constraints and makes proving slower; there is no branch misprediction to avoid; and some memory operations are far more expensive than the compiler thinks. So a standard optimization can help on a CPU and backfire on a zkVM, and until this work no one had measured how often, or why.

  4. This paper's idea

    Systematically measure the mismatch, find the true costs, and make the compiler zkVM-aware

    This paper runs the first systematic study of how compiler optimizations affect zkVMs: 64 individual LLVM passes, six optimization levels, and an unoptimized baseline, across 58 benchmarks on two real RISC-V zkVMs (RISC Zero and SP1). It finds standard optimization helps far less than on a CPU and sometimes reverses, pins the true cost to two components — the number of instructions actually executed and the cost of paging memory — derives four optimization principles from them, and applies those in under 100 lines of LLVM changes to make passes zkVM-aware, improving execution time by up to 45 percent.

Why this problem is worth solving

The entire appeal of a zkVM is that it hides cryptography behind a familiar toolchain, and that same abstraction is what lets a mismatched cost model slip in unnoticed.

A zero-knowledge virtual machine is a system that turns proof generation into a standard compiler backend. A developer writes a normal program in a language like Rust or C++, compiles it with an ordinary toolchain to a standard instruction set such as RISC-V, and the zkVM then emulates that binary, records an execution trace of every instruction it ran, and a proving backend produces a cryptographic proof that the trace is a correct execution of the program. This replaces the old, painful workflow of hand-crafting arithmetic circuits, which was both hard and error-prone, and it inherits mature infrastructure for free: LLVM to compile, and techniques like GPU-accelerated proving and precompiled routines for heavy primitives. zkVMs are already deployed in blockchain systems and are central to major roadmaps, but performance is the standing hurdle and a competitive battleground among vendors. Because the whole point is that developers use conventional languages and compilers, whatever those compilers do — good or bad for proving — is applied by default, which is exactly why it is worth checking what they actually do.

The reason ordinary compiler wisdom can mislead here is that a zkVM prices computation on a completely different basis than a processor.

In a zkVM, the cost of a program is not how fast a processor could run it but how expensive it is to prove it ran. Every instruction the program executes is turned into a set of algebraic constraints, and the proving effort grows with how many constraints there are, so the dominant cost is essentially the number of instructions actually executed — and, unlike on a CPU, instructions have roughly uniform cost, so a multiply is no cheaper than an add and a division is not the expensive operation it is on hardware. Other things invert too: cache misses carry no special penalty because there is no cache, but paging memory in and out is very costly; there is no parallelism, no out-of-order execution, and no branch predictor, so a branch is cheap and a misprediction is a non-concept. A compiler that assumes the usual hardware cost model will therefore make choices that are right for a CPU and wrong here. Understanding that the true currency is executed-instruction count and paging, not cycles saved by hardware tricks, is what lets the compiler be fixed.

What people did before, and where it broke

  • Hand-craft arithmetic circuits for zero-knowledge proofs. Before zkVMs, express the computation directly as a low-level circuit for the proof system. Where it broke. Writing circuits by hand is hard and error-prone, which kept zero-knowledge proofs out of reach for most developers and motivated zkVMs in the first place.
  • Compile zkVM programs with the standard toolchain unchanged. Reuse LLVM and the usual optimization levels so developers keep their normal workflow. Where it broke. The optimizer targets CPU features the zkVM lacks, so its improvements are muted and occasionally reversed; the community assumed higher optimization levels simply help, without measuring their real effect on proving.
  • Trust individual optimizations that are known to help on CPUs. Rely on classic wins like replacing an expensive division with cheaper shifts and adds, hoisting work out of loops, or eliminating branches. Where it broke. Under the proof-centric cost model these can hurt: strength reduction turns one division into several instructions and raises the constraint count; loop-invariant code motion can explode paging cost; branch elimination can force both paths to be executed and proven.
  • Reason about zkVM performance anecdotally. Optimize proving with intuition and one-off observations about what seems slow. Where it broke. Without a systematic study across many passes, levels, benchmarks, and both proving backends, there was no reliable picture of which optimizations matter, which backfire, or why — so no principled way to improve the compiler.
  • The gap. No one had measured, across the space of compiler optimizations and on real zkVMs, how much standard optimization actually helps proof generation, identified the underlying cost components that drive it, or turned that understanding into concrete, zkVM-aware compiler changes — leaving a large, untapped opportunity for compiler-level speedups sitting unexamined.

How the study is put together

From a systematic measurement to zkVM-aware compiler passes

  1. new

  2. new

  3. new

  4. new

  5. new

The idea

The compiler is optimizing against the wrong price list, so first write the right one. Every surprising result in the study traces back to a single mismatch: LLVM prices operations the way a CPU experiences them, but a zkVM charges by proof constraints. The clearest example is strength reduction, the classic optimization that rewrites a division as a shift-and-add sequence because division is slow on hardware — on x86 the transformed code runs 3.5 times faster, but on a zkVM proving gets 40 percent slower, because one division became several instructions and every instruction adds constraints. The fix is not to abandon the optimizer but to give it a cost model that reflects the real machine: on a zkVM a multiply costs the same as an add, division is not expensive, and memory is cheap until it triggers paging. Building a compiler whose transformations are driven by a cost model faithful to an unconventional execution substrate, rather than one inherited from commodity hardware, is the same discipline the compiler-for-in-memory-accelerators work covered elsewhere on this site is built on, teaching the compiler what operations truly cost on a target that behaves nothing like a standard CPU.

Measure the whole space first, then let the data name the principles. Rather than guess which optimizations matter, the paper runs the first systematic sweep — dozens of passes, every optimization level, an unoptimized baseline, dozens of benchmarks, both major proving backends, three metrics each — and lets the measurements reveal structure. The structure it finds is that two cost components explain almost everything: the number of instructions actually executed, and paging. From those, four crisp principles fall out, each validated empirically, about when inlining, loop unrolling, branch elimination, and memory-moving optimizations help or hurt. That evidence then points directly at where a superoptimizer or a zkVM-specific pass could go further. Grounding compiler improvement in a broad empirical study of what the optimizer actually does on the real target, and using the findings to steer the search for better transformations, is the same evidence-first stance the superoptimization-explorer work covered elsewhere on this site takes, exploring the space of optimizations against measured behavior instead of trusting inherited heuristics.

Chase executed-instruction count, because on a zkVM that is the performance. Once the cost model is right, the dominant lever is simple to state: reduce how many instructions the program actually executes, and proving gets proportionally cheaper. The study shows inlining is the single most beneficial pass precisely because it eliminates call overhead and cuts executed instructions, while loop-invariant code motion is the most harmful because it can multiply paging. So the zkVM-aware principles all reduce to spending an optimization only when it lowers the dynamic instruction count: inline when it shrinks executed instructions, unroll only when it does the same, keep branches when eliminating them would execute both sides. When the refined compiler cuts the fibonacci benchmark executed-instruction count by 50 percent, its zkVM execution time drops by 45 percent almost exactly in step. Optimizing against a measured, execution-level notion of the true bottleneck — here the count of instructions that actually run — rather than a static proxy is the same profile-grounded philosophy the profile-guided optimizer work covered elsewhere on this site relies on, letting what the program really does at run time drive the compiler’s choices.

Following an optimization decision through a zkVM

Why strength reduction helps a CPU but hurts a zkVM

A concrete case from the paper: the same standard optimization, applied to the same code, has opposite effects on a CPU and a zkVM, and the refined compiler makes the zkVM-correct choice.

Step 1 of 5

The program computes a remainder or division

A benchmark contains an integer division or remainder operation. On a conventional CPU, a hardware division instruction is comparatively slow, so the compiler looks for a cheaper way to compute the same result.

This is a textbook target for strength reduction, one of LLVM's standard optimizations.

Why it works

  • A faithful cost model fixes the decisions at the source. LLVM assumes CPU costs, so it trades a cheap zkVM operation for several instructions → but updating the RISC-V cost model to zkVM prices (uniform instructions, cheap division, costly paging) informs every pass → so optimizations like strength reduction stop backfiring and the compiler keeps the compact code.
  • Executed-instruction count is the right target, and it is measurable. On a zkVM proving cost tracks the number of instructions actually run, not hardware cycles → but the study confirms this empirically and ties gains to instruction-count reductions → so cutting executed instructions, as inlining does, yields proportional speedups, up to 45 percent on fibonacci from a 50 percent instruction cut.
  • Paging is a hidden cost that some passes inflate. Memory looks cheap under a cache-oriented model, so passes freely move and spill it → but page-ins and page-outs are very expensive on zkVMs, and loop-invariant code motion can raise paging cycles sharply → so avoiding paging-heavy transformations removes the worst regressions, such as a 2.7-times proving slowdown.
  • Branches are cheap here, so eliminating them can backfire. CPU compilers remove branches to dodge misprediction penalties → but a zkVM has no misprediction and predicating a branch forces both paths to execute and be proven → so being conservative about branch elimination avoids adding executed instructions and proving cost.
  • Disabling irrelevant passes costs nothing and cleans up. Some passes exist only to exploit CPU features like out-of-order superscalar execution → but a zkVM runs strictly in order in a circuit, so those passes give no measurable gain → so turning off speculative-execution-style passes removes wasted work with no downside.

What it costs (trade-offs and scope)

  • The gains are real but modest on average, and backend-dependent. The refined compiler improves the highest optimization level on 39 of 58 benchmarks on RISC Zero (average about 4.6 percent) and 19 of 58 on SP1 (average about 1 percent), with the headline 45 percent reserved for cases like fibonacci where instruction count drops sharply. The average uplift is small, and SP1 benefits less than RISC Zero, so this is an opening result, not a solved problem.
  • It is a study plus lightweight tweaks, not a new zkVM compiler. The changes are deliberately small — a zkVM cost model, adjusted heuristics, and disabled passes, under 100 lines in a few hours — and are meant to demonstrate the opportunity. The paper explicitly frames zkVM-specific passes, backends, and superoptimizers as future work, so the deeper wins it points to are not yet built.
  • Findings are tied to two zkVMs and one toolchain. The results cover RISC Zero and SP1 with LLVM on RISC-V; differences already appear between the two backends (some effects differ due to their intermediate representations and constraint-level optimizations), so conclusions may shift for other proving systems, cost models, or non-RISC-V zkVMs.
  • The cost model is a strong simplification. Treating instructions as roughly uniform and reducing performance to executed-instruction count and paging captures most of the behavior, but it is an approximation; precompiles for heavy primitives, floating-point emulation, recursion, and constraint-level details all complicate the picture, and a pass judged by instruction count alone could still miss second-order effects.
  • Benchmark scope shapes the numbers. The 58 benchmarks emphasize cryptographic, mathematical, and loop- and memory-heavy kernels; the reported averages and the ranking of passes reflect that mix, and a different workload distribution could change which optimizations matter most and by how much.

The evaluation

Optimizing a program for a zkVM three ways

PropertyUnoptimizedStandard LLVM (highest level)zkVM-aware LLVM
What the optimizer targets
Effect versus no optimizationBaseline
Handling of a division (strength reduction)Keeps the division
Loop-invariant code motionNot applied
Effort and scopeNone
From the paper's study on RISC Zero and SP1 across 58 benchmarks and 71 optimization profiles. Standard optimization helps zkVMs (over 40 percent) but far less than on CPUs; the zkVM-aware changes beat the highest standard level on 39 of 58 benchmarks on RISC Zero (average about 4.6 percent) and 19 of 58 on SP1 (average about 1 percent), reaching 45 percent where executed-instruction count drops most.Underlined values explain themselves when clicked.

Check yourself

Check your understanding

  1. 1. Why might a standard compiler optimization that speeds up a CPU slow down a zkVM?

  2. 2. What two cost components does the study identify as driving zkVM performance?

  3. 3. What did the zkVM-aware compiler changes consist of, and what did they achieve?

  4. 4. How should the results be read in terms of scope and significance?

Sources

The paper is “Evaluating Compiler Optimization Impacts on zkVM Performance” by Thomas Gassmann, Stefanos Chaliasos, Thodoris Sotiropoulos, and Zhendong Su (ETH Zurich, with Chaliasos at the UCL Centre for Blockchain Technologies and zkSecurity), published at ASPLOS 2026, DOI 10.1145/3779212.3790159.

This explainer is written from the full text of the authors’ arXiv version (arXiv:2508.17518). Everything above comes from the paper’s own sections: the introduction and background framing zkVMs as compiling ordinary programs to RISC-V, emulating them to record an execution trace, and proving that trace, with a cost model where every instruction becomes constraints and hardware features like caches, branch prediction, and instruction-level parallelism are absent; the two motivating examples of optimizations that reverse on zkVMs (strength reduction, 3.5 times faster on x86 but 40 percent slower proving on RISC Zero, and a second raising SP1 proving by 5 percent); the study design (58 benchmarks, 71 optimization profiles comprising 64 individual LLVM passes, six optimization levels, and an unoptimized baseline, on RISC Zero and SP1, measured by cycle count, execution time, and proving time, with the three research questions on individual passes, combinations, and comparison to native CPUs); the findings that standard levels improve zkVM performance over 40 percent but far less than on CPUs, that inline is the most beneficial pass (around 28.4 percent gains, roughly 30 percent cycle-count reductions) and loop-invariant code motion the most harmful (a 2.7-times proving slowdown on npb-lu from a 444 percent paging-cycle increase); the two cost components (dynamic instruction count and paging cycles); the four optimization principles (avoiding paging pressure, selective inlining, unrolling only when it reduces executed instructions, and conservative branch elimination); and the zkVM-specific changes (a cost model via the RISC-V target classes, updated heuristics and defaults, and disabled CPU-only passes such as speculative execution), which in under 100 lines improved execution time by up to 45 percent on fibonacci (from a 50 percent instruction-count reduction), beating the highest standard level on 39 of 58 benchmarks on RISC Zero (average about 4.6 percent) and 19 of 58 on SP1 (average about 1 percent) with consistent proving-time gains, and the discussion of future directions in zkVM-specific passes, backends, and superoptimizers.

Related papers on Compilers & Programming Languages

See all 162 →