Enable the wide integer extension (XReviveVec) - #594
Draft
kvpanch wants to merge 18 commits into
Draft
Conversation
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.
xermicus
reviewed
Aug 14, 2026
xermicus
left a comment
Member
There was a problem hiding this comment.
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)
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
sltuto produce and another to consume, and anything wider than two registers is passed by reference: 56 bytes for oneadd 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.