Skip to content

perf(compiler): lower bulk copies and zeroing to sol_memmove_ and sol_memset_ - #22

Merged
Mister-Meeseeks merged 5 commits into
CrocSwap:mainfrom
paulbalaji:opt-memcpy-syscalls
Oct 19, 2026
Merged

Mister-Meeseeks merged 5 commits into
CrocSwap:mainfrom
paulbalaji:opt-memcpy-syscalls

Conversation

@paulbalaji

@paulbalaji paulbalaji commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Problem

copy, large struct assignment and zeroing compile to byte loops. On account-heavy programs that's the largest CU overhead we measured, e.g. 5,232 CU to zero a 1 KiB tail.

Solution

On fogo-v0 and solana-v3, lower bulk moves to sol_memmove_ and zeroing to sol_memset_. That covers copy, append, string building, struct and array copies of 48 bytes or more, zeroed values of 96 bytes or more, the entry duplicate-account copy, the ResizeAccount fill, and two exact zeroing-loop forms.

  • sol_memcpy_ is never used: it fails on overlap, including self-assignment.
  • The threshold is 1 byte, because the syscall (~15 CU) beats both byte and word loops at every size measured.
  • Host C and legacy v1.51 builds are unchanged.

Benchmarks

workload before after
zero 1 KiB tail 5,232 124
unaligned 32-byte copy 389 137
ResizeAccount to 10 KiB 53,529 941
Hyperlane mailbox Init (Rust: 12,610) 25,182 13,372
Hyperlane mailbox fixture suite 1,047,475 696,597 (-33.5%)

The goSVM examples get 1.4% to 13% cheaper, with no fixture step worse. On solana-v3, 35 of 5,480 probe vectors got up to 1,006 CU slower from changed inlining in unrelated code.

Tests

examples/memory-values against native Go: 13,660 host vectors and 5,480 per SBF profile, built three ways (threshold 1, threshold 64, loops only). It covers overlap, aliasing, zero length, nil, threshold edges and near-miss loops that must not be rewritten. The frozen corpus's C is byte-identical once the new syscall blocks are stripped. The full-swap and clock2 reference ELFs change.

Linked work

Closes #5. Includes #12's commit. Used by hyperlane-xyz/hyperlane-monorepo#9852.

memmoveHelper prepends only a u8 typedef to libraryMemory, whose overlap
test casts pointers to u64, so linking the helper always failed with
"use of undeclared identifier 'u64'". Every library-free program for which
LLVM emits a memmove call, for example a large struct assigned through two
pointers that may alias, could not be built.

Tests: the helper compiles on its own under -Werror, and a probe whose
pointer-to-pointer copies of a 545-byte value (including a self-assignment)
lower to memmove builds and produces the expected account bytes on fogo-v0
and solana-v3. Both fail before this change.
goSVM moved bytes with C loops: gosvm_move for copy, append and string
building, and the hidden memcpy/memset/memmove LLVM calls for aggregate
assignment and zeroing. On SBF the byte loop costs about 8 CU per byte and
the word loop 6 CU per 8 bytes; the memory syscalls cost max(10, n/250).

On fogo-v0 and solana-v3, SBF builds now define GOSVM_MEMORY_SYSCALLS and
those routines hand every move of at least one byte to sol_memmove_ or
sol_memset_ (measured: the syscall wins from 1 byte at every alignment).
The entrypoint's duplicate-descriptor copy and ResizeAccount's zero fill do
the same, and two zeroing loops (for i := range s { s[i] = 0 } and
for j := e; j < uint64(len(s)); j++ { s[j] = 0 }) are emitted as a memset.
sol_memcpy_ is never used: the runtime rejects overlapping ranges, including
src == dst. Zero-length moves never reach the VM. Host C and target-less
legacy builds compile the original loops in #else branches, so the legacy
ELFs are unchanged.

Tests: examples/memory-values against native Go as host C (13,660 vectors,
SDK 1 and 2 routines) and on both SBF profiles (5,480 vectors) built with
threshold 1, threshold 64 and loops only; runtime, read-only write and
legacy-identity SBF tests; the corpus check now proves the emitted C is the
recorded C plus only the syscall blocks.
GO_ALLOCATION gains the bulk-memory and zeroing-loop sections,
BACKEND_INTERFACE says why a backend must not use sol_memcpy_ for copy,
ERROR_CONTRACT records the change as CU/ELF only, and HANDOFF and
TARGET_PROFILES point at the new reference ELFs, which verify_rebuild.py
now uses by default.

results/compiler/2026-10-19-memory-syscalls holds the threshold curves,
before/after CU for the Hyperlane Go mailbox (Init 25,182 -> 13,372 CU on
fogo-v0, all 64 steps -33.5%), the goSVM examples (-1.4% to -13%, no step
worse) and the port patterns (1 KiB tail zeroing 5,232 -> 124 CU), plus the
fresh-rebuild check.
The memset branch of a rewritten zeroing loop took temporary numbers, so
the #else branch (host and legacy builds) named its temporaries one higher
than earlier compilers did. The branch now reuses the numbers inside its
own block. examples/memory-values' C minus the syscall blocks is now the
previous compiler's C, and its legacy ELF and the full swap's are
unchanged; SBF ELFs are unchanged by this commit.
@paulbalaji paulbalaji changed the title Move bulk memory with sol_memmove_ and sol_memset_ on modern SBF perf(compiler): lower bulk copies and zeroing to sol_memmove_ and sol_memset_ Oct 19, 2026
@Mister-Meeseeks
Mister-Meeseeks merged commit 57f4770 into CrocSwap:main Oct 19, 2026
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.

copy and zeroing compile to byte loops instead of the memmove/memset syscalls

2 participants