Skip to content

perf: prove U256 multiply, shifts and compare with Int256 instructions - #679

Draft
Qumeric wants to merge 1 commit into
develop-v2.1.0from
valery/ruint-int256
Draft

perf: prove U256 multiply, shifts and compare with Int256 instructions#679
Qumeric wants to merge 1 commit into
develop-v2.1.0from
valery/ruint-int256

Conversation

@Qumeric

@Qumeric Qumeric commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

OpenVM proves a 256-bit arithmetic operation in a single instruction, and the bigint extension is
already enabled in openvm.toml — but nothing was reaching those instructions, because ruint does
the arithmetic in software limb loops. This patches the guest's ruint to dispatch to them.

Benchmark results

Both blocks measured against develop-v2.1.0 at the same commit, execute-metered.

metric block 24001988 block 24002549
execute_metered_insns 588,618,511 → 562,861,248 (−4.376%) 527,179,427 → 508,428,606 (−3.557%)
metered_rows_unpadded −4.993% −3.868%
metered_main_cells_unpadded −2.158% −1.829%
metered_interaction_cells_unpadded −3.749% −3.033%
metered_memory_unpadded_bytes −2.867% −2.492%
app segments 65 → 60 (−5) 59 → 56 (−3)

Runs: block 24001988 baseline / this PR; block 24002549 baseline / this PR.

Three things worth knowing about the shape of it.

Not every operation is worth dispatching. The instructions address memory while ruint's methods
take operands by value, so LLVM has to spill both operands and the result to stack slots the
instruction can point at — about twenty loads and stores it cannot elide, because the inline asm!
may read any memory. Measured per operation on riscv64, in the shape revm calls them:

op portable native
arithmetic_shr 164 48
wrapping_shl 102 40
wrapping_shr 94 40
wrapping_mul 65 36
cmp / lt 37 / 32 1 / 8
wrapping_add 27 36
wrapping_sub 27 36

So multiply, the shifts and comparison are dispatched; addition, subtraction and the bitwise
operations deliberately are not — a four-limb carry chain is cheaper than the staging. Comparison
avoids the staging entirely because its operands arrive as references.

The shifts dispatch from the Shl/Shr operator impls, not the inherent methods. extern "C"
cannot be called from a const fn, and nybbles builds lookup tables in statics with
wrapping_shl, so making that non-const breaks a downstream crate. Trait methods are never const,
and revm's handlers use the operators, so this keeps the win with no const fallout.

It requires openvm-bigint-guest linked for its symbols. Nothing references it in source, so it
needs use openvm_bigint_guest as _; or the rlib is dropped before its #[no_mangle] exports reach
the link — the same idiom stateless-executor already uses for openvm-guest-mem.

Correctness: 2.8M differential cases against stock ruint, with the instruction shims mocked from
the reference implementation the circuit itself is checked against, so a disagreement would be a
disagreement with the circuit. That caught one real trap — the shift instructions read only the low
byte of the shift amount, so amounts >= 256 wrap instead of clearing, and the wrappers resolve that
themselves.

Open question before this leaves draft. It depends on a ruint fork
(Qumeric/uint, branch perf/openvm-int256), patched into this workspace only. wrapping_mul and
arithmetic_shr have to drop const there, which is a breaking change for other ruint users, so
that part is not upstreamable as written. Upstream is already unifying const_* helpers with runtime
twins (alloy-rs/ruint#615), which is the shape an upstreamable version would take — but until then
this is a fork we carry. That trade is the only thing blocking this PR: the measurement is the
largest in the current queue, and the segment reduction is the largest of any change in flight.

Patches the guest's ruint to dispatch those operations to OpenVM's 256-bit instructions
instead of limb loops, and links the shims that define them. Addition, subtraction and the
bitwise operations stay portable: the instructions address memory while ruint's methods take
their operands by value, so the operand staging outweighs their few register operations.
@Qumeric
Qumeric force-pushed the valery/ruint-int256 branch from 9098b93 to bec9d6f Compare July 28, 2026 06:33
@Qumeric
Qumeric changed the base branch from valery/guest-keccak-sponge to develop-v2.1.0 July 28, 2026 06:33
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.

1 participant