Skip to content

perf(compiler): compare hand-written byte loops a word at a time on SBF - #31

Draft
paulbalaji wants to merge 4 commits into
CrocSwap:mainfrom
paulbalaji:opt-byte-compare-loops
Draft

paulbalaji wants to merge 4 commits into
CrocSwap:mainfrom
paulbalaji:opt-byte-compare-loops

Conversation

@paulbalaji

@paulbalaji paulbalaji commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Problem

for i := 0; i < 32; i++ { if a[i] != b[i] { return false } } stays a byte loop on SBF, about 10 CU per byte, so an equal 32-byte key check costs ~320 CU. #29 covers == on arrays, but programs write the loop to compare a slice with an array, or to get the first difference (Rust's derived Ord).

Solution

When the start and bound are constants, the variable is declared by the loop, the body is only if a[i] != b[i] { ... } with no else, and a and b are variables of type []E, [N]E or *[N]E (E a one-byte integer), SBF builds put a word compare in front of the unchanged loop. If both operands hold the bytes (a pointer is non-nil) and they are all equal, the loop is skipped: it would have run to the end without entering the if or trapping. Otherwise the loop runs as before, so the first different byte decides and a short operand or nil pointer traps at the same index. A tail shorter than a word is compared as the last 8 bytes, overlapping the word before. Host C keeps only the loop; GOSVM_BYTE_LOOP_WORDS turns the path on in host C for tests.

The other idiom in #26, little-endian assembly (v |= uint64(b[i]) << (8*i)), needs nothing: clang already combines it into one ldxdw, with a constant or a variable offset. A word path for it compiled to the same SBF code in isolation and cost 3 to 7 CU more in the example program, so it is left out and noted in GO_ARRAYS.md.

Benchmarks

One small program per shape comparing input bytes 0..31 with 32..63 against main 939b215, whole transaction, same on both profiles (the empty probe costs 98 CU):

shape equal, main this PR first byte differs, main this PR
[]byte, 32 bytes 422 112 106 110
[]byte vs *[32]byte 512 148 135 152
[32]byte arrays, Ord body (plus two copies) 523 175 180 183
[]byte, 13 bytes 152 109 99 106
[]byte, 8 bytes 132 106 99 106

An equal 32-byte compare goes from ~320 CU to ~15. A difference costs 3 to 17 CU more, since the word compare runs before the loop finds it.

Tests

  • TestByteLoopDifferential: examples/byte-loop-values natively and as host C with and without the word paths, 2,050 vectors over five compare shapes (arrays, slices, an array pointer, bytes 4 to 12, int8 with a body that breaks) and five load shapes: equal, bit 0 and bit 7 flipped at every byte, every operand length 0 to 32 with and without an earlier difference, nil pointers. Outputs and panics match Go.
  • TestByteLoopSBF runs the same vectors on fogo-v0 and solana-v3: 1,741 complete with Go's output, 309 trap and roll back, as in Go.
  • TestByteLoopsC: the five compares get the path; nine near-misses (offset index, len bound, outer variable, extra statement, wider elements, short array, ==, else, non-variable operand) don't.
  • TestCorpusMatchesPreCheckCompiler strips the paths and matches every recorded hash.
  • make test: same failures as main 939b215 on this machine (stdlib provenance/features tests with Go 1.23.2, TestFullSwapFacts under /tmp).
  • verify_rebuild.py + check_rebuild_reference.py pass locally: the full-swap reference ELFs change (clock2 doesn't), so results/compiler/2026-10-19-byte-loops/rebuild/metrics.json is the new default reference.

Linked work

Closes #26 (the hand-written byte loop row). #27, #28, #29 and #30 cover the other rows; if this merges first, #26 should stay open until the rest land.

Same family as #29. Used by hyperlane-xyz/hyperlane-monorepo#9852 through hyperlane-integration-4.

A loop like `for i := 0; i < 32; i++ { if a[i] != b[i] { ... } }` stays
a byte loop on SBF, about 10 CU per byte. When the bounds are constants,
the variable is the loop's own, the body is only that if, and the
operands are byte slices, arrays or array pointers held in variables,
SBF builds now XOR the bytes a word at a time first and skip the loop
when they are all equal. Otherwise the unchanged loop runs, so the first
different byte, short operands and nil pointers behave as before. Host
C keeps only the loop.
The change moves the full-swap reference ELFs, so CI's fresh-rebuild check now compares against this branch's rebuild/metrics.json, the default reference in scripts/verify_rebuild.py.
paulbalaji added a commit to hyperlane-xyz/hyperlane-monorepo that referenced this pull request Oct 19, 2026
hyperlane-integration-4 (3ddeff0) is upstream main 939b215, which merged
our earlier PRs #9 to #24 in CrocSwap/goSVM#25, plus the open draft PRs:
whole-word runtime loads, stores and key comparisons (CrocSwap/goSVM#27),
solana.FindAddress on sol_try_find_program_address, also behind
pda.Seeds.FindAddress (CrocSwap/goSVM#28), word-at-a-time == on byte
arrays (CrocSwap/goSVM#29), the exact MinimumBalanceFor fast path at
thresholds 2.0 and 1.0 (CrocSwap/goSVM#30), and word compares in front of
hand-written byte compare loops (CrocSwap/goSVM#31). The SDK snapshot is
copied from the new CLI.
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.

Byte-at-a-time runtime and codegen add 0.3k to 4k CU to small instructions

1 participant