Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
5a09e97
Enable the wide integer extension (XReviveVec)
kvpanch Aug 14, 2026
6b1b602
book: update the wide integer extension measurements
kvpanch Aug 17, 2026
01ce83f
Lower the EVM word operations to the XReviveVec intrinsics
kvpanch Aug 20, 2026
8907943
benchmarks: compare the four ways of carrying a wide instruction's width
kvpanch Aug 27, 2026
a1edd46
Emit ReviveV2 blobs and document wide-extension performance
Aug 26, 2026
51932b7
docs: add XReviveVec complete reference and experiment data
Sep 1, 2026
7d1ded5
newyork: default un-inferred value width to i256
Sep 1, 2026
7590bb4
book: correct recompiler wide-op timing in the analysis chapter
Sep 1, 2026
2b320b5
docs: rebuild the book
Sep 1, 2026
1b64b7c
Bump the llvm and revive-differential-tests submodules
Sep 1, 2026
1f56efc
docs: record the i512 wide load/store fix and correct the arena ceiling
Sep 2, 2026
1ac946b
deps: point the polkavm crates at the git branch, not local paths
Sep 2, 2026
302e266
docs: add the dedicated-register-file (XReviveW) experiment
kvpanch Sep 2, 2026
691b174
docs: XReviveW div/rem fixed; refresh the experiment numbers
kvpanch Sep 2, 2026
1a7c9f0
docs: note the XReviveW move-width correctness fix (review)
kvpanch Sep 2, 2026
08af4d3
harness: add differential value-correctness check (diffcheck.py)
kvpanch Sep 2, 2026
aeb2ada
docs: refresh §5 recompiler microbench (inline vs trampoline) and exp…
kvpanch Sep 3, 2026
5ba5119
docs: re-measure all benchmarks on the NewYork IR corpus (type infere…
kvpanch Sep 3, 2026
ff9e8e1
docs: measure the extension on top real mainnet production contracts …
kvpanch Sep 7, 2026
e787c8b
docs: add per-benchmark interp/recomp breakdown for ref/vec/w (§12)
kvpanch Sep 8, 2026
d52b1b0
docs: add per-benchmark code-size tables (.text + blob) for ref/vec/w…
kvpanch Sep 8, 2026
4b44d35
docs: describe what each real production contract does (§12)
kvpanch Sep 8, 2026
a34516b
docs: decompose why gas increases on real contracts (§12)
kvpanch Sep 8, 2026
d183ab0
docs: tighten prose throughout, all data tables retained
kvpanch Sep 8, 2026
e8cf3cb
docs: per-instruction gas (§5a) + new x86-lowering reference doc
kvpanch Sep 10, 2026
1fe0fa8
docs(x86-lowering): full unrolled i256 sequences + why-not-vector-reg…
kvpanch Sep 10, 2026
0be7be4
docs: decompose the toy-corpus +18% gas, correcting the "converts onl…
kvpanch Sep 10, 2026
ff390b4
docs: document the gas-charging code path (§6d)
kvpanch Sep 10, 2026
5d029da
docs: recalibrate wide gas to N x 64-bit; ext gas now <= scalar (§5a/…
kvpanch Sep 10, 2026
0294de2
bench: re-run all benchmarks with recalibrated gas; per-benchmark gas…
kvpanch Sep 10, 2026
9d5216a
docs: set_width-aware gas implemented + tested (§11)
kvpanch Sep 10, 2026
8e28e97
docs: measured set_width inference opportunity = 0 (§11)
kvpanch Sep 10, 2026
3e013d9
bench: full corpus with set_width (width-aware) gas enabled vs disabl…
kvpanch Sep 10, 2026
c3ed6b2
docs: record measured set_width-on-vs-off run (1.000x, all 64) in §11
kvpanch Sep 10, 2026
04a0ed5
bench: re-run corpus after wtrunc 2->1 -- gas now at parity
kvpanch Sep 10, 2026
47a8d4b
bench: corpus with wtrunc=0 -- vec 0.932x / w 0.904x ref (63/64 below…
kvpanch Sep 10, 2026
7e2bd81
docs: bring gas sections current (wtrunc=0, 0.932x/0.904x), narrowing…
kvpanch Sep 15, 2026
7ff9e32
docs: embed full per-benchmark gas table + CallGas Yul/NewYork IR div…
kvpanch Sep 15, 2026
8294309
docs: single combined per-benchmark gas table + trace the Yul/NewYork…
kvpanch Sep 15, 2026
1350f48
docs: combined table uses NewYork+w column; generalize the memory-op …
kvpanch Sep 15, 2026
94cc13e
docs: gas-level category decomposition of the CallGas outlier (Yul-w …
kvpanch Sep 15, 2026
68dce0b
docs: full NY+w corpus remeasure — narrowing and the extension are su…
kvpanch Sep 15, 2026
59a4546
docs: root-cause the NY-vs-default width difference to the inference …
kvpanch Sep 15, 2026
cd6b168
docs: clarify the i160-vs-zext point — default path has no width prop…
kvpanch Sep 15, 2026
7a575bc
docs: mechanism for why the extension hurts on narrowed code
kvpanch Sep 15, 2026
181b77d
docs: regenerate §6d combined gas table with no empty cells
kvpanch Sep 17, 2026
711dcd6
bench+docs: set_width testing knob + NewYork+W (no set_width) full-me…
kvpanch Sep 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[submodule "llvm"]
path = llvm
url = https://github.com/llvm/llvm-project.git
branch = release/21.x
url = https://github.com/paritytech/llvm-project.git
branch = kvpanch/vec_ext_custom_inst
[submodule "revive-differential-tests"]
path = revive-differential-tests
url = https://github.com/paritytech/revive-differential-tests.git
44 changes: 16 additions & 28 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 4 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,10 @@ which = "8.0"
path-slash = "0.2"
rayon = "1.11"
clap = { version = "4", default-features = false, features = ["derive"] }
polkavm-common = "0.35.0"
polkavm-linker = "0.35.0"
polkavm-disassembler = "0.35.0"
polkavm = "0.35.0"
polkavm-common = { git = "https://github.com/paritytech/polkavm.git", branch = "kvpanch/vec_ext_custom_inst" }
polkavm-linker = { git = "https://github.com/paritytech/polkavm.git", branch = "kvpanch/vec_ext_custom_inst" }
polkavm-disassembler = { git = "https://github.com/paritytech/polkavm.git", branch = "kvpanch/vec_ext_custom_inst" }
polkavm = { git = "https://github.com/paritytech/polkavm.git", branch = "kvpanch/vec_ext_custom_inst" }
alloy-primitives = { version = "1.4", features = ["serde"] }
alloy-sol-types = "1.4"
alloy-genesis = "1.1.2"
Expand Down

Large diffs are not rendered by default.

950 changes: 950 additions & 0 deletions benchmarks/analysis/vsetivli-tradeoff/CallGas.Other.yul.optimized.ll

Large diffs are not rendered by default.

348 changes: 348 additions & 0 deletions benchmarks/analysis/vsetivli-tradeoff/HANDOFF.html

Large diffs are not rendered by default.

161 changes: 161 additions & 0 deletions benchmarks/analysis/vsetivli-tradeoff/RESULTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
# How XReviveVec should carry a wide instruction's width

Four ways of answering "how wide is this `revive.wadd`?", measured end to end: LLVM IR in, PolkaVM
blob out, run on the interpreter.

| arm | width comes from | LLVM | linker |
|---|---|---|---|
| `ref` | — (no extension) | — | — |
| `no-vsetvli` | `funct7[6:5]` of the instruction | per-width mnemonics, one custom-2 space | reads it off the instruction |
| `vsetvli-noMO` | `vtype`, set by `vsetivli` | one mnemonic per operation; outliner barred from lifting vtype-dependent code | CFG dataflow |
| `vsetvli-MO` | `vtype`, set by `vsetivli` | outlining allowed | interprocedural dataflow |

Corpus: 103 modules of post-optimization IR at `-Oz` from the 15-contract benchmark set.

## The gas column is not a performance measurement

`runblob` sets no cost model, so PolkaVM falls back to `CostModel::naive()`, in which *every*
instruction costs one gas -- a 256-bit `revive.wmul` included. There is no better option available:
`CostModelKind::Full` routes through `simulator.rs`, which `unimplemented!()`s on every wide
instruction and would panic. Assigning real costs to the 32 wide instructions is still open work.

So "gas" below means **dynamic instruction count**, and against `ref` it flatters the extension
badly: one `revive.wmul` standing in for a ~40-instruction limb chain scores as a 39-instruction
saving when the real work is nothing like 1/40th. **Do not read the gas/ref column as a speed-up.**

Between the three extension arms the figure is more defensible -- they execute the same wide
instructions in the same places and differ mainly in call and outlining overhead -- but it is still
a count, not a time.

Everything ran on the **interpreter**. The recompiler is x86-64 and requires BMI2; the measuring
host is arm64, so it could not run at all. A real performance number needs either gas costs for the
wide instructions or an x86-64 Linux host.

## Results

Code and blob size over the 61 modules every arm compiled, linked and ran. Gas over the 108
exports every arm ran to the same outcome after the same number of host calls — an arm that faults
early burns little gas while doing none of the work, so unmatched exports would read as a speed-up.

| arm | `.text` | blob | gas | text/ref | **blob/ref** | gas/ref |
|---|---|---|---|---|---|---|
| `ref` | 193,400 | 242,022 | 313,765 | — | — | — |
| `no-vsetvli` | 154,642 | 167,905 | 262,162 | −20.04% | −30.62% | **−16.45%** |
| `vsetvli-noMO` | 171,140 | **165,935** | 264,975 | −11.51% | **−31.44%** | −15.55% |
| `vsetvli-MO` | 164,758 | 176,311 | 265,135 | −14.81% | −27.15% | −15.50% |

Compile time, fastest of three runs over the 83 modules `llc` compiled in every arm:

| arm | compile |
|---|---|
| `ref` | 7.3 s |
| `no-vsetvli` | 5.0 s |
| `vsetvli-noMO` | 5.2 s |
| `vsetvli-MO` | 5.4 s |

Link time is 0.9–1.1 s across all arms; the dataflow does not show up in it.

## Every figure above is a sum, and the sums are concentrated

The size and gas columns add up 61 modules; one of them is 24.6% of the total blob bytes and the
top five are 49.8%. Compile time comes from a separate run over 83 modules, so it does not share
the others' basis. Read the totals as "what the corpus costs in total", not as "what a typical
contract costs".

Per module, `vsetvli-noMO` against `no-vsetvli` on blob size, over the 94 modules both linked:

| | |
|---|---|
| sum | −3.20% |
| mean | −0.10% |
| **median** | **+1.58%** |
| smaller on | 33 modules |
| larger on | 61 modules |

So width-from-`vtype` is *not* smaller on a typical contract. It is smaller on big ones — −3.54%,
−4.74% and −6.69% on the three largest — and costs a near-constant ~19 bytes on small ones. The
claim the totals support is that it scales better, not that it always wins.

Dropping the `ref` column widens the comparison from 61 modules to 94, because 33 were excluded
only because `ref` cannot lower the `revive.*` intrinsics at all:

| arm (94 modules) | `.text` | blob | blob vs funct7 |
|---|---|---|---|
| `no-vsetvli` | 1,028,558 | 1,345,724 | — |
| `vsetvli-noMO` | 1,290,866 | 1,302,659 | **−3.20%** |
| `vsetvli-MO` | 1,177,362 | 1,511,771 | **+12.34%** |

## What the numbers say

**The object-level penalty does not survive translation.** At `.text` level `funct7` looks decisively
better: −20.04% against −11.51%, a 10.7-point gap, most of it the outlining that width-in-`vtype`
gives up. At blob level that gap closes and reverses in aggregate: `vsetvli-noMO` totals the
smallest blob of any arm, though see the per-module distribution above before reading that as a
uniform win.
The `vsetivli` instructions carry no PolkaVM instruction and the linker drops them, and PolkaVM's
own optimizer recovers what the machine outliner was doing. Measuring RISC-V `.text` overstates the
cost of `vtype` by an order of magnitude.

**Outlining is not worth having.** `vsetvli-MO` is the largest of the three extension arms at blob
level -- +12.34% against `funct7` over the 94-module set -- and the slowest to compile, and it needs
the most linker machinery. Whatever the outliner
saves in RISC-V is more than lost by the time PolkaVM has finished with it.

**Instruction count barely distinguishes them.** All three land within one point of each other
(−16.45%, −15.55%, −15.50%), which is the one comparison the naive cost model supports. On the
evidence available the width mechanism is a compile-time and link-time question; whether it is also
a performance one is untested.

**Compilation is faster with the extension than without,** by about 30% — i256 as a machine type is
less work than expanding it into limb chains. `vtype` costs 4–8% against `funct7`.

## Correctness

`funct7` cannot get the width wrong: it is in the instruction. `vtype` can, and did.

Two real defects were found and fixed while building this:

- **`vtype` was assumed to survive calls but nothing restored it.** `hasCallPreservedVType()`
returned true for the extension, so a caller reconfigured nothing after a call, while
`CalleeSavedRegs<(add ..., VL, VTYPE)>` never took effect because `VTYPE` is a *reserved*
register and reserved registers are not spilled as callee-saved. A caller at m2 calling a
function that configured m4 resumed at m4: reloads read 64 bytes where 32 were spilled. Now
behind `-riscv-revive-call-preserved-vtype`, off by default.
- **Relocated wide accesses did not link.** A `%lo` on `revive.wld`/`revive.wst` had no handler, in
either design — 27 per module in ERC20 alone. Pre-existing, and fixed in both.

**A residual defect remains.** In 3 of 103 modules (XENCrypto variants) a wide instruction sits
directly after a call with no configuration between it and the call, in the same basic block, so no
other predecessor can supply one:

```
dc0: jalr ra # call
dc4: revive.wzext v8, zero # runs under whatever the callee left
```

The linker's dataflow reports this rather than guessing, which is how it was found. It is an
LLVM-side bug in the `vtype` design, not a linker limitation, and it is a class of bug `funct7`
does not have.

## Recommendation

`vsetvli-noMO`, on the strength of how it scales: it totals the smallest blob, wins by 3.5-6.7% on
the largest contracts, keeps custom-1 and custom-3 free, and costs about 4% compile time and one
point of instruction count against `funct7`. The recommendation rests on code size alone --
performance was not measured, see above. Against it: it is ~1.58% *larger* on the median contract, so if small
contracts dominate what actually ships, `funct7` is the better choice and this recommendation should
flip. The residual post-call defect must be fixed
first — it is three modules, but it is a wrong-code bug, not a link failure.

`vsetvli-MO` should be dropped: it is bigger, slower to compile, and needs interprocedural analysis
in the linker to buy a regression.

## Reproducing

```
benchmarks/analysis/vsetivli-tradeoff/compare.py # .text and compile time
benchmarks/analysis/vsetivli-tradeoff/sweep.py # blob, gas, link time, end to end
```

`bin/` holds the two `llc` builds and the two `polkatool` builds the arms need; `*.patch` are the
working-tree changes each was built from.
Loading