Skip to content

Add vector registers and custom instructions - #596

Open
xermicus wants to merge 19 commits into
mainfrom
cl/custom-ops
Open

Add vector registers and custom instructions#596
xermicus wants to merge 19 commits into
mainfrom
cl/custom-ops

Conversation

@xermicus

@xermicusxermicus commented Aug 17, 2026

Copy link
Copy Markdown
Member

This implements XReviveVec vendor 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:

ContractWithoutWithChange
erc115530,64916,243−47.1%
erc2042,89321,907−49.0%
erc72149,42323,981−51.5%
oz_gov81,15940,147−50.6%
oz_rwa37,97518,303−51.8%
oz_simple_erc2016,5547,806−52.8%
oz_stable39,03217,733−54.6%
proxy3,5772,727−23.8%
total301,262148,847−50.6%

The custom instructions:

Details

The wide instructions

OpcodeNameShapeMeaning
231wide_addw ← w, wwrapping addition
232wide_subw ← w, wwrapping subtraction
233wide_mulw ← w, wwrapping multiplication
234wide_andw ← w, wbitwise and
235wide_orw ← w, wbitwise or
236wide_xorw ← w, wbitwise exclusive or
237wide_div_unsignedw ← w, wdivision; by zero yields zero
238wide_div_signedw ← w, wsigned division; by zero yields zero
239wide_rem_unsignedw ← w, wremainder; by zero yields zero
240wide_rem_signedw ← w, wsigned remainder, taking the dividend’s sign; by zero yields zero
241wide_expw ← w, wexponentiation, wrapping
242wide_sign_extend_bytew ← w, wsign extension from the byte the second source names; an index past 31 leaves the value alone
114wide_add_modw ← w, w, waddition modulo the fourth operand, on the untruncated sum; a zero modulus yields zero
115wide_mul_modw ← w, w, wmultiplication modulo the fourth operand, on the full 512-bit product; a zero modulus yields zero
243wide_set_equalx ← w, wone if equal, zero otherwise
244wide_set_not_equalx ← w, wone if not equal
245wide_set_less_than_unsignedx ← w, wunsigned less than
246wide_set_less_than_signedx ← w, wsigned less than
247wide_shift_logical_leftw ← w, xshift left; an amount of 256 or more clears the value
248wide_shift_logical_rightw ← w, xshift right, filling with zeroes; 256 or more clears the value
249wide_shift_arithmetic_rightw ← w, xshift right, filling with the sign; 256 or more fills the value with it
165wide_shift_logical_left_immw ← w, immshift left by a constant
166wide_shift_logical_right_immw ← w, immshift right by a constant
167wide_shift_arithmetic_right_immw ← w, immarithmetic shift right by a constant
250wide_movew ← wa copy
251wide_reverse_bytesw ← wthe bytes in the opposite order
252wide_to_regx ← wthe low 64 bits
116wide_count_set_bitsx ← wthe number of set bits
117wide_count_leading_zero_bitsx ← wleading zeroes; 256 for a zero value
118wide_count_trailing_zero_bitsx ← wtrailing zeroes; 256 for a zero value
253wide_from_reg_unsignedw ← xthe register, zero extended
254wide_from_reg_signedw ← xthe register, sign extended
112wide_loadw ← [x + imm]32 bytes, little endian
113wide_store[x + imm] ← w32 bytes, little endian
162wide_load_imm_unsignedw ← immthe immediate, widened as a register would be: sign extended to 64 bits, then taken unsigned
163wide_load_imm_signedw ← immthe immediate, sign extended the whole way
164wide_load_absolutew ← [imm]32 bytes from a fixed address

The vector instructions

OpcodeNameShapeMeaning
181vector_configimmsets vtype and the element count from the immediate; the count is capped at what the configuration holds
182vector_config_dynamicx ← x, immsets vtype from the immediate and the count from the register, capped the same way; writes the settled count back
12vector_config_dynamic_discardx, immthe same, discarding the settled count
21vector_arithmeticimmone element-wise operation; the immediate carries the operation, its operand shape and its registers: twenty-seven operations, each against a register group, a general purpose register or a five-bit immediate
168vector_movev ← va whole register copy
177vector_loadv ← [x + imm]16 bytes, one whole register
178vector_store[x + imm] ← v16 bytes, one whole register
183vector_load_u8v ← [x + imm]vl bytes, unit stride; the tail is left alone
184vector_load_u16v ← [x + imm]vl halfwords, unit stride
185vector_load_u32v ← [x + imm]vl words, unit stride
186vector_load_u64v ← [x + imm]vl doublewords, unit stride
187vector_store_u8[x + imm] ← vvl bytes, unit stride
188vector_store_u16[x + imm] ← vvl halfwords, unit stride
189vector_store_u32[x + imm] ← vvl words, unit stride
119vector_store_u64[x + imm] ← vvl doublewords, unit stride
169vector_set_equalv ← v, vone mask bit per element; the bits past the count are cleared
176vector_set_not_equalv ← v, vthe complementary comparison
17vector_set_equal_immv ← v, immequality against one value in every element
18vector_set_not_equal_immv ← v, immits complement
2vector_mask_andv ← v, vbitwise over the low vl bits of two masks
3vector_mask_and_notv ← v, va & !b
4vector_mask_orv ← v, va | b
5vector_mask_xorv ← v, va ^ b
6vector_mask_nandv ← v, v!(a & b)
7vector_mask_norv ← v, v!(a | b)
8vector_mask_or_notv ← v, va | !b
9vector_mask_xnorv ← v, v!(a ^ b)
179vector_count_maskx ← vthe set bits among the low vl
11vector_count_mask_maskedx ← vthe same, counting only the elements the mask in v0 selects
22vector_first_maskx ← vthe index of the first set bit, or minus one
23vector_first_mask_maskedx ← vthe same, through the mask in v0
24vector_extractx ← vthe first element, sign extended
14vector_splatv ← xthe value written to every active element
13vector_splat_immv ← immthe immediate, sign extended, written to every active element
15vector_insertv ← xthe first element only, the rest left alone
19vector_insert_immv ← immthe same, from an immediate
16vector_element_indexveach active element’s own index

kvpanchand others added 14 commits August 14, 2026 07:51
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>
@socket-security

socket-securityBot commented Aug 17, 2026

Copy link
Copy Markdown

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.

ActionSeverityAlert (click "▶" to expand/collapse)
WarnHigh
High CVE: cargo hickory-proto: NSEC3 closest-encloser proof validation enters unbounded loop on cross-zone responses

CVE:GHSA-3v94-mw7p-v465 hickory-proto: NSEC3 closest-encloser proof validation enters unbounded loop on cross-zone responses (HIGH)

Affected versions: >= 0.25.0-alpha.3 <= 0.25.2

Patched version: No patched versions

From:?cargo/hickory-proto@0.25.2

ℹ Read more on: This package | This alert | What is a CVE?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Remove or replace dependencies that include known high severity CVEs. Consumers can use dependency overrides or npm audit fix --force to remove vulnerable dependencies.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore cargo/hickory-proto@0.25.2. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

WarnHigh
High CVE: cargo rustls-webpki: Denial of service via panic on malformed CRL BIT STRING

CVE:GHSA-82j2-j2ch-gfr8 rustls-webpki: Denial of service via panic on malformed CRL BIT STRING (HIGH)

Affected versions: < 0.103.13; >= 0.104.0-alpha.1 < 0.104.0-alpha.7

Patched version: 0.103.13

From:?cargo/rustls-webpki@0.101.7

ℹ Read more on: This package | This alert | What is a CVE?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Remove or replace dependencies that include known high severity CVEs. Consumers can use dependency overrides or npm audit fix --force to remove vulnerable dependencies.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore cargo/rustls-webpki@0.101.7. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

WarnHigh
High CVE: Yamux vulnerable to remote Panic via malformed Data frame with SYN set and len = 262145

CVE:GHSA-vxx9-2994-q338 Yamux vulnerable to remote Panic via malformed Data frame with SYN set and len = 262145 (HIGH)

Affected versions: < 0.13.10

Patched version: 0.13.10

From:?cargo/yamux@0.12.1

ℹ Read more on: This package | This alert | What is a CVE?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Remove or replace dependencies that include known high severity CVEs. Consumers can use dependency overrides or npm audit fix --force to remove vulnerable dependencies.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore cargo/yamux@0.12.1. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

View full report

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>
@michalkucharczyk

michalkucharczyk commented Aug 19, 2026

Copy link
Copy Markdown

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.

configurationwallvs host (portable)
stock PVM — no wide instructions at all63.21 µs2.57×
current — fused add/sub folds (aa0dff7)38.58 µs1.56×

xermicusand others added 4 commits August 20, 2026 18:34
Bumps the submodule from f5d444d to fcaadf0 (2026-08-17), pulling in
#311-#315 (NY workloads, custom benchmark platforms, Cargo compiler,
w3s workload fix, opcode profiling & weight reporting).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Cyrill Leutwiler <bigcyrill@hotmail.com>
Signed-off-by: xermicus <cyrill@parity.io>
Signed-off-by: xermicus <cyrill@parity.io>
Sign up for freeto 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.

3 participants

@xermicus@michalkucharczyk@kvpanch