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

@xermicus xermicus 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:

Contract Without With Change
erc1155 30,649 16,243 −47.1%
erc20 42,893 21,907 −49.0%
erc721 49,423 23,981 −51.5%
oz_gov 81,159 40,147 −50.6%
oz_rwa 37,975 18,303 −51.8%
oz_simple_erc20 16,554 7,806 −52.8%
oz_stable 39,032 17,733 −54.6%
proxy 3,577 2,727 −23.8%
total 301,262 148,847 −50.6%

The custom instructions:

Details

The wide instructions

Opcode Name Shape Meaning
231 wide_add w ← w, w wrapping addition
232 wide_sub w ← w, w wrapping subtraction
233 wide_mul w ← w, w wrapping multiplication
234 wide_and w ← w, w bitwise and
235 wide_or w ← w, w bitwise or
236 wide_xor w ← w, w bitwise exclusive or
237 wide_div_unsigned w ← w, w division; by zero yields zero
238 wide_div_signed w ← w, w signed division; by zero yields zero
239 wide_rem_unsigned w ← w, w remainder; by zero yields zero
240 wide_rem_signed w ← w, w signed remainder, taking the dividend’s sign; by zero yields zero
241 wide_exp w ← w, w exponentiation, wrapping
242 wide_sign_extend_byte w ← w, w sign extension from the byte the second source names; an index past 31 leaves the value alone
114 wide_add_mod w ← w, w, w addition modulo the fourth operand, on the untruncated sum; a zero modulus yields zero
115 wide_mul_mod w ← w, w, w multiplication modulo the fourth operand, on the full 512-bit product; a zero modulus yields zero
243 wide_set_equal x ← w, w one if equal, zero otherwise
244 wide_set_not_equal x ← w, w one if not equal
245 wide_set_less_than_unsigned x ← w, w unsigned less than
246 wide_set_less_than_signed x ← w, w signed less than
247 wide_shift_logical_left w ← w, x shift left; an amount of 256 or more clears the value
248 wide_shift_logical_right w ← w, x shift right, filling with zeroes; 256 or more clears the value
249 wide_shift_arithmetic_right w ← w, x shift right, filling with the sign; 256 or more fills the value with it
165 wide_shift_logical_left_imm w ← w, imm shift left by a constant
166 wide_shift_logical_right_imm w ← w, imm shift right by a constant
167 wide_shift_arithmetic_right_imm w ← w, imm arithmetic shift right by a constant
250 wide_move w ← w a copy
251 wide_reverse_bytes w ← w the bytes in the opposite order
252 wide_to_reg x ← w the low 64 bits
116 wide_count_set_bits x ← w the number of set bits
117 wide_count_leading_zero_bits x ← w leading zeroes; 256 for a zero value
118 wide_count_trailing_zero_bits x ← w trailing zeroes; 256 for a zero value
253 wide_from_reg_unsigned w ← x the register, zero extended
254 wide_from_reg_signed w ← x the register, sign extended
112 wide_load w ← [x + imm] 32 bytes, little endian
113 wide_store [x + imm] ← w 32 bytes, little endian
162 wide_load_imm_unsigned w ← imm the immediate, widened as a register would be: sign extended to 64 bits, then taken unsigned
163 wide_load_imm_signed w ← imm the immediate, sign extended the whole way
164 wide_load_absolute w ← [imm] 32 bytes from a fixed address

The vector instructions

Opcode Name Shape Meaning
181 vector_config imm sets vtype and the element count from the immediate; the count is capped at what the configuration holds
182 vector_config_dynamic x ← x, imm sets vtype from the immediate and the count from the register, capped the same way; writes the settled count back
12 vector_config_dynamic_discard x, imm the same, discarding the settled count
21 vector_arithmetic imm one 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
168 vector_move v ← v a whole register copy
177 vector_load v ← [x + imm] 16 bytes, one whole register
178 vector_store [x + imm] ← v 16 bytes, one whole register
183 vector_load_u8 v ← [x + imm] vl bytes, unit stride; the tail is left alone
184 vector_load_u16 v ← [x + imm] vl halfwords, unit stride
185 vector_load_u32 v ← [x + imm] vl words, unit stride
186 vector_load_u64 v ← [x + imm] vl doublewords, unit stride
187 vector_store_u8 [x + imm] ← v vl bytes, unit stride
188 vector_store_u16 [x + imm] ← v vl halfwords, unit stride
189 vector_store_u32 [x + imm] ← v vl words, unit stride
119 vector_store_u64 [x + imm] ← v vl doublewords, unit stride
169 vector_set_equal v ← v, v one mask bit per element; the bits past the count are cleared
176 vector_set_not_equal v ← v, v the complementary comparison
17 vector_set_equal_imm v ← v, imm equality against one value in every element
18 vector_set_not_equal_imm v ← v, imm its complement
2 vector_mask_and v ← v, v bitwise over the low vl bits of two masks
3 vector_mask_and_not v ← v, v a & !b
4 vector_mask_or v ← v, v a | b
5 vector_mask_xor v ← v, v a ^ b
6 vector_mask_nand v ← v, v !(a & b)
7 vector_mask_nor v ← v, v !(a | b)
8 vector_mask_or_not v ← v, v a | !b
9 vector_mask_xnor v ← v, v !(a ^ b)
179 vector_count_mask x ← v the set bits among the low vl
11 vector_count_mask_masked x ← v the same, counting only the elements the mask in v0 selects
22 vector_first_mask x ← v the index of the first set bit, or minus one
23 vector_first_mask_masked x ← v the same, through the mask in v0
24 vector_extract x ← v the first element, sign extended
14 vector_splat v ← x the value written to every active element
13 vector_splat_imm v ← imm the immediate, sign extended, written to every active element
15 vector_insert v ← x the first element only, the rest left alone
19 vector_insert_imm v ← imm the same, from an immediate
16 vector_element_index v each active element’s own index

kvpanch and 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-security Bot 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.

Action Severity Alert  (click "▶" to expand/collapse)
Warn High
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.

Warn High
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.

Warn High
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.

configuration wall vs host (portable)
stock PVM — no wide instructions at all 63.21 µs 2.57×
current — fused add/sub folds (aa0dff7) 38.58 µs 1.56×

xermicus and 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 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.

3 participants