Uh oh!
There was an error while loading. Please reload this page.
Add vector registers and custom instructions - #596
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.
Fast-forwards the submodule to 27228a06b512, the XReviveVec commit rebased onto LLVM 22.1.8. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The linker now decodes the custom-2 encodings and the interpreter implements them, so a contract compiled with `+xrevivevec` gets all the way to a PVM blob for the first time. Point the polkavm and polkadot-sdk dependencies at the checkouts that have the support, until it is released. Four EVM operations had instructions waiting for them that nothing emitted: `addmod`, `mulmod`, `exp` and `signextend` still went to the `stdlib.ll` routines they exist to replace, over ten kilobytes of code between them. They are looked up rather than assumed, so a build linked against an LLVM without the extension keeps working and falls back to the routines. The unrolled byte swap is skipped for the same reason: `llvm.bswap.i256` is one instruction now, so splitting a word into four limbs has nothing left to win. Over the openzeppelin contracts the blobs are **50.1% smaller**, and `codesize.json` moves the same way on both pipelines: on newyork, `DivisionArithmetics` 7,370 to 957, `ERC20` 8,726 to 4,065, `SHA1` 6,264 to 2,946. `make format` drops `--all`: with the two dependencies overridden to local paths, it walks into those checkouts and reformats code this repository does not own. Every workspace member is still covered. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The overview said 45%, which was the figure before the encodings were tightened, and it said nothing about how a wide instruction is actually encoded. Give the numbers per contract and describe the two forms that got them there: a destination that repeats the first source is left out of the encoding, and a widened constant is folded into the instruction that widens it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The wide shifts and the load from a fixed address take their operand as an immediate now, which is another 1,574 bytes over the openzeppelin contracts and puts the extension at -50.6%. `codesize.json` moves with it on both pipelines. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`op_boundary_sweep` picks its tuples by hand, and for division and reduction that means a handful of divisors, none of them near the top of the range. The instructions behind those opcodes are new, and their one interesting failure mode only shows for a divisor or modulus at or above 2^255, so the cases that matter were the ones missing. Cross the boundary values instead: the limb boundaries, the signed extremes and either side of 2^255, for `div`, `mod`, `sdiv` and `smod`, and against high moduli for `addmod` and `mulmod`. The reduction sweep catches a dropped carry in the 512-bit reduction, which until now only the `mulmod` fuzz noticed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The page described holding wide values in the vector registers as an earlier design, which reads as a prior iteration rather than what it is: the design the extension was specified to have. Name the departure, give the reason as what enabling the vector extensions drags in rather than anything about the registers, and say which of the Phase 1 results stop applying because the configurations they compared no longer exist. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
PolkaVM now implements the vector extensions at a fixed length of 128 bits, so a contract compiled by a toolchain without revive's LLVM fork can use them. The wide instructions are the layer above that: `i256` in a vector register pair, one instruction per wide operation, and a calling convention that passes wide values in registers rather than by reference. The vector length needs no compiler argument, because `xrevivevec` is defined for a machine whose length is exactly the 128 bits its `Zvl` states. What is passed is one argument turning off a whole-register-move workaround for hardware that mishandles `vtype`, which PolkaVM does not, and which cost a kilobyte across the openzeppelin contracts. Over those contracts blobs are 50.6% smaller, 301,262 bytes against 148,847, and `codesize_newyork.json` is byte-identical to what the earlier arrangement with a private register file recorded. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The fork's exact-vector-length support resized the spill slots it makes itself, but the generic creators of scalable stack objects, an alloca of a scalable type or a stack temporary out of instruction selection, size theirs at the length-agnostic minimum and rely on the scalable stack ID to say so. The hook answered Default, which dropped the tag at creation and left such an object undersized: an alloca of a vscale x 2 x i64 got sixteen bytes, and the 32-byte store through it overwrote the saved return address laid out next to it. The hook keeps the tag now, and frame finalization restates every tagged object to its real size before the scalable region is laid out. Nothing in the corpus creates such an object today, so blobs are byte-identical; the reproducer allocates a 48-byte frame with the object at its full 32 bytes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Setting RESOLC_DISABLE_WIDE_INTEGERS in the environment compiles without the extension. That is what produces the baseline the extension's numbers are measured against, keeps a bisection able to cross the commit that introduced it, and gives a blob a way to target a PolkaVM release without the instructions. An environment variable rather than a flag because resolc spawns itself recursively per contract, and the environment is inherited where a flag would have to be threaded through every process boundary. The switch also closes a latent mismatch: the extension's intrinsics were declared whenever the linked LLVM had them, while selecting them requires the +xrevivevec feature the target machine requests. Both sides now consult the same function, so an intrinsic can never be emitted into a module whose features cannot select the instruction behind it. With the variable set, ERC20 builds at exactly the old baseline, 8,726 bytes on the newyork pipeline; without it, at exactly the current 4,012. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The recompiler implements the wide and vector instructions now, out of the same implementation the interpreter runs, so the page no longer says it refuses them. The status section documents the off switch, and the dashes the style guide bans are swept out of both pages, with the minus signs in the tables normalized to the ASCII hyphen everything else uses. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The fork's commits now live on cl/custom-ops rather than on the personal branch, which picked up unrelated work. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The four polkavm crates were path overrides into a local checkout while the wide instruction support only existed there. It is pushed now, so the dependencies point at the polkavm repository's cl/more-instr branch, pinned by the lock file to the commit that implements the recompiler support. The polkadot-sdk override stays local until its own release. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The setup line still named the old personal branch and a checkout on the original author's machine. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review the following changes in direct dependencies. Learn more about Socket for GitHub. |
Warning [Security] Socket has found a problem with the dependencies from this PR. Check the details below to solve the issue. If the affected dependency is unreachable, we still recommend you to use a patched version. Remember: according to Parity's Security Policy, you must resolve all "Block" alerts before proceeding. It is recommended to resolve "Warn" alerts too. Learn more about Socket for GitHub.
|
The llvm bump makes XReviveVec self-contained: it no longer implies the vector extensions, so the compiler cannot emit a standard vector instruction and the whole-register-move workaround flag has nothing left to act on. The openzeppelin blobs lose the dead configuration instructions the old arrangement left behind, 63 bytes across the corpus, and the extension page now says plainly that what PolkaVM implements of the vector instructions is a subset of Zve64x, targeted by nothing: resolc cannot emit it, and a stock toolchain built for the real extension will generally not link against the subset. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Would be nice to check what new instructions will bring in terms of the performance for parachain runtimes. I won't have time in the near future to carry on this task, so leaving some guide if anyone wants to pick it up. What needs to be done:
I executed the benchmarks for my vibe-coded (optimized after few iterations) 256-bits instructions, here are the results for the record / reference. Measurements done on one of the toaster machines.
|
Signed-off-by: Cyrill Leutwiler <bigcyrill@hotmail.com>
Signed-off-by: xermicus <cyrill@parity.io>
Signed-off-by: xermicus <cyrill@parity.io>
This implements
XReviveVecvendor extension. Half of the Zve64x with Zvl128b registers. On top: i256 as a machine type held in a vector register pair, one custom-2 instruction per wide operation (arithmetic, compares, shifts, addmod/mulmod/exp/signextend, bit counts, 32-byte load/store, conversions), and the calling convention passing wide values in registers with the non-argument pairs callee-saved.This effectively halves code sizes:
The custom instructions:
Details
The wide instructions
The vector instructions