Skip to content

Enable the wide integer extension (XReviveVec) - #594

Draft
kvpanch wants to merge 18 commits into
mainfrom
kvpanch/vec_ext_custom_inst
Draft

Enable the wide integer extension (XReviveVec)#594
kvpanch wants to merge 18 commits into
mainfrom
kvpanch/vec_ext_custom_inst

Conversation

@kvpanch

@kvpanch kvpanch commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

EVM words are 256 bits and RISC-V registers 64, so every wide operation becomes a four-limb chain. With no carry flag each carry costs an sltu to produce and another to consume, and anything wider than two registers is passed by reference: 56 bytes for one add i256, and a 192-byte frame to call with three of them.

Request +xrevivevec, which makes i256 a machine type held in VRM2 -- one instruction per wide operation, arguments in registers. -30% code over the 15 benchmark contracts.

Experimental: the PolkaVM linker cannot decode the custom-2 encodings, so on this branch resolc fails at link for every contract.

EVM words are 256 bits and RISC-V registers 64, so every wide operation becomes a
four-limb chain. With no carry flag each carry costs an `sltu` to produce and
another to consume, and anything wider than two registers is passed by reference:
56 bytes for one `add i256`, and a 192-byte frame to call with three of them.

Request `+xrevivevec`, which makes i256 a machine type held in VRM2 -- one
instruction per wide operation, arguments in registers. -30% code over the 15
benchmark contracts.

Experimental: the PolkaVM linker cannot decode the custom-2 encodings, so on this
branch resolc fails at link for every contract.
@kvpanch
kvpanch requested review from elle-j and xermicus August 14, 2026 11:51

@xermicus xermicus left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thats a good first indicator thanks! Small nit to start with: Can we name the extension somelike XRreviveWide, the Vec in XReviveVec confused me slightly (you'd think it's a custom vector extension which isn't very accurate I think)

kvpanch and others added 17 commits August 17, 2026 14:16
Re-dumped the benchmark IR with the current compiler. The previous figures came
from dumps that were ten days stale, and the drift mattered: __mul, recorded as a
gap worth 6,492 bytes, had already been fixed in revive, and llvm.umul.with.overflow.i256
(47 sites), which current revive does emit, was absent entirely.

Headline is now -35.29% combined and -39.78% code, against -36.52% and -40.90%
before; the extension's benefit is unchanged, revive's own baseline is smaller.
`addmod`, `mulmod`, `exp` and `signextend` called hand-written routines in
stdlib.ll, even though the extension defines an instruction and an intrinsic
for each. Emit the intrinsics instead. All four routines are then dead-code
eliminated, and with `__mulmod` goes the last `i512` in the corpus: the
widening multiply in its body was the only thing that used the width.

`signextend` takes the value first and the byte index second -- the shift
operand order, not the EVM opcode's -- so `bytes` is passed second.

-3,962 bytes of code and -2,016 of rodata over the 103 benchmark modules,
-0.35% total. `mulmod` and `addmod` account for the win; `exp` costs a little,
because an opaque intrinsic cannot be constant-folded the way a call to an
ordinary function could.
Measures no-extension, width-in-funct7, and width-from-vtype with and without
outlining, from IR through to a blob running on the interpreter.

At .text level funct7 wins by ten points; at blob level that reverses, because
the vsetivli instructions carry no PolkaVM instruction and its optimiser
recovers what the outliner was doing. The totals hide the distribution, though:
vtype is larger on 61 of 94 modules and wins only on the big ones.

The gas column is instruction count, not cost -- every wide instruction is one
gas under the naive model, and the simulator refuses to model them at all -- so
performance remains unmeasured.
- Link at TargetInstructionSet::ReviveV2 and repoint the polkavm dependency
  at the local 0.37 tree, so resolc's integrated linker emits the wide
  instructions instead of raising an ICE on the new encodings
- book: add a per-instruction execution-performance section comparing the
  extension against the scalar reference, at 128 and 256 bits

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Consolidated analysis of the XReviveVec wide-integer extension across revive,
LLVM and PolkaVM: the instruction set, ABI and LMUL support; interpreter vs
recompiler execution (with the default inline lowerings and their trampoline
fallback); and the full per-benchmark and per-instruction data, code size,
compile time, execution, and recalibrated gas figures.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`inferred_width` returned the value's `min_width`, which for a value the
inference pass never constrained is `I1` -- the narrowing lattice's bottom, not
a sensible codegen width -- so codegen truncated every un-inferred value to a
single bit. Return `I256` (a full EVM word) when no constraint was recorded and
only narrow when inference actually proved a smaller width. Also gate the
inference pass behind `NEWYORK_NO_TYPE_INFERENCE` so the no-narrowing baseline
can be measured (every value then stays at the i256 default).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The chapter reported the recompiler's wide compute at ~1,700 ns and called the
trampoline dominant. That figure was a test-build logging artifact; the real
out-of-line cost is ~40-60 ns per cheap op (a small multiple of the ~23 ns the
shared routine takes plus register save/restore). Update the numbers and the
surrounding narrative.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Regenerated mdbook output, including the new wide-integer analysis and
extension pages and the refreshed search index and table of contents.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Advance llvm to release/22.x 99031a4 and revive-differential-tests to 69cc64e.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Note the >i256 looped load/store path and the 32-bit-pointer bug (now fixed)
that trapped i512+ wide memory on the recompiler, and correct the native-code
arena's address-space bound (below 8 GiB via the zygote's recycle() assert, a
4 GiB ceiling — not the 12 GiB gap to VMCTX).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the absolute /home/... path dependencies on polkavm, polkavm-common,
polkavm-linker and polkavm-disassembler with a git dependency on
paritytech/polkavm (branch kvpanch/vec_ext_custom_inst), so revive resolves
from a clean checkout instead of a machine-specific path. Cargo.lock is pinned
to the branch tip once that branch is published (cargo cannot resolve the
unpushed commits yet).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Document the alternative i256 design -- a dedicated W0-W15 file with no vtype,
instead of reused RVV groups -- and its measured head-to-head against vectors
and the scalar reference (doc §11). Recompiler execution is at exact parity;
XReviveW is ~6% smaller object code (no vsetivli) and marginally better on blob,
interpreter and gas, with a known i256 div/rem gap. Adds the measurement harness
(measure_wreg.py) and its results (per-bench-wreg.tsv).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The i256 div/rem gap is root-caused and fixed (setMaxDivRemBitWidthSupported was
128 for the W-path, so ExpandLargeDivRem rewrote i256 division to a software loop
before ISel; raised to 256). Re-ran the corpus with full coverage (80 modules in
both arms, up from 72): recompiler at parity (per-module median 1.000x), W ~7%
smaller object, ~2% smaller blob, -2.4% gas; wall-time aggregate is noise-skewed
so the median is used. Doc §11 and per-bench-wreg.tsv updated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Code review found a second correctness bug: the i256 register move was encoded as
wmv1r (128-bit), dropping the high half of every copy; fixed to wmv2r (256-bit).
Like the div/rem fix it does not change the codesize/gas/wall-time comparison
(same instruction size and count), only computed values.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Compare each contract's observable values (host-call sequence, storage writes as
key->value, return payload -- layout-independent, via runblob RUNBLOB_TRACE)
across ref/vec/w. Result: w is value-identical to vec on all 80 shared modules.
Honest coverage: empty calldata + stubbed hosts exercise only the deploy path and
sink-reaching values -- real but not exhaustive; the wmv truncation bug produces
zero w-vs-vec diffs here (values reach storage via memory, not the register move),
so the lit encoding tests remain the guard for register-level bugs. Documented in
doc §11.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…and §11

§5: re-measured the per-instruction recompiler numbers with inlining as the
default and sandbox worker logging off (the #[test] build otherwise inflates every
trampoline-crossing op by ~1,680 ns of log I/O). The stale table showed a flat ~58
ns for every wide op, making ext look uniformly slower than the recompiler's inline
scalar. The truth is an inline/trampoline split: the inlined cheap ops (add/sub/
bitwise/compares/move/zext/trunc/load/store) now run at or below scalar ref, while
mul/shifts/min/max/bswap/sext still cost a real ~50-60 ns crossing and the heavy
iterative ops are genuinely thousands of ns. Added a "recomp path" column and a
reading guide; microbench-{interp,recomp}.txt regenerated to match.

§11 (XReviveW): added the interpreter and recompiler wall-time columns to the
results (median ratios), a base-ISA ref baseline, a dedicated calling-convention
subsection (i256 in W0-W7, spill to 32B slots, applied in CC_RISCV and FastCC), and
a full "set_width" subsection defining the originally-proposed variable-width mode
instruction and why the prototype omits it (99.9% i256 per §7 -> a width mechanism,
its elimination pass, cross-call mode state, and linker tracking all pay for
generality the corpus does not use; hence i256-only).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nce ON)

Re-ran the ref/vec/w harness against ir-corpus-newyork and compared to the Yul
baseline (compare_newyork.py; per-bench-wreg-{yul,newyork}.tsv). Added a §9
subsection with the result.

Where the NewYork front-end compiles, type inference is a large win: on the 36
modules that compile+run on the vec arm in both corpora, NewYork cuts gas 3.3x
(0.30x), interpreter time 2.6x (0.39x), and code size ~1.5x (.text 0.65x, blob
0.68x), at recompiler parity (0.99x) -- i256->i64/i128 narrowing removes most wide
ops, and the win lands on the deterministic gas metric.

Coverage caveat (the pipeline is still maturing): with the extension on, NewYork
compiles only 37/99 (vec) through llc -- it core-dumps / times out on the rest;
ref (no extension) is unaffected. The dedicated-w path compiles a strict superset
(51 vs 37; 14 vec crashes on, 0 the reverse) because it avoids the fragile
RVV/vtype codegen -- reinforcing §11. So this is a "where it compiles" result, not
a full-corpus replacement for §6.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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