Skip to content

perf: accelerate EVM word byte swap on ARM64 - #12798

Draft
kamilchodola wants to merge 2 commits into
masterfrom
perf/arm64-evm-word-byteswap
Draft

perf: accelerate EVM word byte swap on ARM64#12798
kamilchodola wants to merge 2 commits into
masterfrom
perf/arm64-evm-word-byteswap

Conversation

@kamilchodola

Copy link
Copy Markdown
Contributor

Changes

  • Add an ARM64 AdvSimd fast path for full 32-byte EVM word byte reversal.
  • Preserve the existing AVX-512, AVX2, and scalar implementations.
  • Add an exact 32-distinct-byte regression test and an isolated BenchmarkDotNet benchmark.

Types of changes

What types of changes does your code introduce?

  • Bugfix (a non-breaking change that fixes an issue)
  • New feature (a non-breaking change that adds functionality)
  • Breaking change (a change that causes existing functionality not to work as expected)
  • Optimization
  • Refactoring
  • Documentation update
  • Build-related changes
  • Other: Description

Testing

Requires testing

  • Yes
  • No

If yes, did you write tests?

  • Yes
  • No

Notes on testing

  • Added an exact-order unit test using 32 distinct bytes.
  • Confirmed the .NET 10 intrinsic sequence compiles with zero warnings and errors in a package-independent compiler check.
  • Graviton microbenchmark and end-to-end RPC benchmark are pending; this PR is intentionally opened as a draft.

Documentation

Requires documentation update

  • Yes
  • No

Requires explanation in Release Notes

  • Yes
  • No

Remarks

The ARM64 sequence uses two REV64 operations and two EXT #8 lane rotations, then exchanges the 128-bit halves. This preserves exact full-word reversal while avoiding the scalar four-limb fallback on ARM64.

@claude

claude Bot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Claude finished @kamilchodola's task in 5m 28s —— View job


Review: ARM64 AdvSimd byte swap

  • Gather context / read diff
  • Verify ARM64 intrinsic sequence correctness
  • Check the branch is actually reachable and covered on ARM64
  • Review test & benchmark additions
  • Post review

No Critical or High findings. The intrinsic sequence is correct — I traced the byte algebra rather than taking the PR description at its word, and the composition is a genuine full 32-byte reversal. Details in the inline comment.

What's right

  • Algorithm. REV64 reverses bytes within each 64-bit lane; EXT #8 (index 1 at ulong granularity) swaps the two lanes. Applied per 128-bit half with the halves exchanged in Vector256.Create(lower, upper), this is exactly r[i] = b[31-i]. The test's expected literals check out byte-by-byte.
  • Blast radius is zero on x64. The branch is added below the existing AVX-512/AVX2 checks, and all four IsSupported tests are JIT-time constants that fold away. Existing paths are untouched.
  • The new path is actually executed in CI. nethermind-tests.yml runs Nethermind.Evm.Test on ubuntu-24.04-arm as well as ubuntu-latest, so the AdvSimd branch runs on a real ARM64 runner. That retires the main correctness risk for a change like this.
  • Consistent with repo precedent. Gating on AdvSimd.Arm64.IsSupported while calling base AdvSimd.* matches HexConverter.cs:163, which also uses the same ExtractVector128(x, x, n) rotation idiom.
  • Callers (EvmStack.cs:335/350/362, UInt256Extensions, StorageChange) are genuinely hot, so ARM64 is worth optimizing.

Findings

Severity Finding
Medium Benchmark won't yield a trustworthy number — a sub-nanosecond op timed as a bare [Benchmark] with a loop-invariant _word and a pure AggressiveInlining callee. Needs OperationsPerInvoke + a loop-carried dependency, and ideally [DisassemblyDiagnoser] confirmation. Suggested replacement inline.
Medium The perf claim is unmeasured. This is an optimization-only change with no functional benefit, and the PR body states the Graviton microbenchmark and end-to-end RPC benchmark are still pending. Acknowledged by the author and consistent with the draft status — but the number has to land before merge, otherwise there's nothing justifying the added path.
Low XML <summary> is now incomplete — it enumerates every implementation but omits the ARM64 branch, so it tells an ARM64 reader the scalar fallback applies when it no longer does.
Low Test placement — tests Nethermind.Core.Extensions.EvmWordExtensions from Nethermind.Evm.Test/EvmStackTests.cs. Nethermind.Core.Test is the matching project and is also in the arm64 matrix.
Low Test strength — one identity-permutation vector. A [TestCase]-parameterized differential test against Array.Reverse plus a double-swap round-trip would be stronger and is what AGENTS.md asks for over more literal vectors. Also note no CI leg reaches the scalar fallback any more (x64 → AVX, arm64 → AdvSimd).
Low Consider TBLAdvSimd.Arm64.VectorTableLookup with a reversed index vector does each half in 1 instruction instead of 2. Higher latency on some cores, so worth a third arm in the microbenchmark rather than a blind swap.

One thing worth checking when the benchmark runs: Vector256<T> is not hardware-accelerated on ARM64, so GetLower()/GetUpper()/Vector256.Create may round-trip through the stack. That cost hits the scalar fallback too, so the delta should stay positive — but it could compress the win well below what the raw instruction count suggests.

Security

No concerns. No new public surface, no untrusted-input handling, no allocation, no unsafe blocks.

Verdict

Correctness is sound and I'd be comfortable with the code as written once the doc comment is updated. The gate is the missing measurement — for a perf-only change, the Graviton number is the deliverable.

Note: I could not run dotnet build in this environment (command not permitted), so I verified the intrinsic API usage against existing in-repo usage in HexConverter.cs rather than by compiling. The author reports a clean compile.
• branch perf/arm64-evm-word-byteswap

Comment thread src/Nethermind/Nethermind.Core/Extensions/EvmWordExtensions.cs
Comment thread src/Nethermind/Nethermind.Evm.Benchmark/EvmStackBenchmarks.cs Outdated
Comment thread src/Nethermind/Nethermind.Evm.Test/EvmStackTests.cs Outdated
@github-actions

Copy link
Copy Markdown
Contributor

EXPB Benchmark Comparison

Run: View workflow run

superblocks

Scenario: nethermind-flat-superblocks-perf-arm64-evm-word-byteswap-delay0s
No cached master baseline for superblocks. A baseline will be created from the next successful master push run.

Client Processing (SSE)

Metric PR
AVG (ms) 876.28
MEDIAN (ms) 836.9
P90 (ms) 1038.9
P95 (ms) 1172.4
P99 (ms) 2904.3
MIN (ms) 575.4
MAX (ms) 2904.3
K6 TTFB
Metric PR
AVG (ms) 1856.00
MEDIAN (ms) 1329.33
P90 (ms) 3780.08
P95 (ms) 4422.15
P99 (ms) 4782.51
MIN (ms) 691.35
MAX (ms) 7880.38

realblocks

Scenario: nethermind-flat-realblocks-perf-arm64-evm-word-byteswap-delay0s
No cached master baseline for realblocks. A baseline will be created from the next successful master push run.

Client Processing (SSE)

Metric PR
AVG (ms) 20.54
MEDIAN (ms) 17.8
P90 (ms) 33.7
P95 (ms) 40.4
P99 (ms) 87.6
MIN (ms) 0.3
MAX (ms) 200.6
K6 TTFB
Metric PR
AVG (ms) 24.64
MEDIAN (ms) 21.00
P90 (ms) 37.98
P95 (ms) 45.34
P99 (ms) 95.58
MIN (ms) 1.00
MAX (ms) 324.81

fusaka

Scenario: nethermind-flat-fusaka-perf-arm64-evm-word-byteswap-delay0s
No cached master baseline for fusaka. A baseline will be created from the next successful master push run.

Client Processing (SSE)

Metric PR
AVG (ms) 46.41
MEDIAN (ms) 36.7
P90 (ms) 77.5
P95 (ms) 112.8
P99 (ms) 220.8
MIN (ms) 5.4
MAX (ms) 339.9
K6 TTFB
Metric PR
AVG (ms) 56.55
MEDIAN (ms) 45.78
P90 (ms) 94.30
P95 (ms) 121.66
P99 (ms) 235.92
MIN (ms) 7.81
MAX (ms) 654.35

@kamilchodola

Copy link
Copy Markdown
Contributor Author

@claude re-review

@claude

claude Bot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Claude finished @kamilchodola's task in 5m 25s —— View job


Re-review: ARM64 AdvSimd byte swap (commit 19264d4)

  • Diff the follow-up commit against my prior findings
  • Verify the new Nethermind.Core.Test fixture (placement, API availability, oracle strength)
  • Verify the benchmark rewrite
  • Re-confirm the intrinsic sequence is unchanged
  • Confirm via CI (could not build locally — dotnet not permitted in this environment)
  • Post review

All four code-level findings from the first pass are resolved. The intrinsic sequence is now empirically confirmed on real ARM64 hardware, not just by my byte-algebra traceRun Nethermind.Core.Test (ubuntu-24.04-arm) passed on this commit, and that leg executes the new AdvSimd branch.

Prior findings — status

Prior Status
Medium — benchmark won't yield a trustworthy number Resolved. OperationsPerInvoke = 4 + serial word = word.ByteSwap() chain. Loop-carried dependency blocks hoisting across BDN's unroll; the return value blocks DCE. Detail →
Medium — perf claim unmeasured ⚠️ Still open, acknowledged by author. See below.
Low — XML <summary> incomplete Resolved. EvmWordExtensions.cs:19 now documents the ARM64 path alongside the other three.
Low — test placement Resolved. Moved to Nethermind.Core.Test/EvmWordExtensionsTests.cs. Root placement matches the sibling convention there (EncodingExtensionsTests.cs, SizeExtensionsTests.cs, TransactionExtensionsTests.cs). Verified Nethermind.Core.Test is in the ubuntu-24.04-arm matrix (nethermind-tests.yml:52-63).
Low — test strength Resolved, and better than what I suggested.

On the rewritten test

EvmWordExtensionsTests.cs is a genuine improvement over both the original and my own suggestion:

  • MemoryMarshal.Read<EvmWord> + Array.Reverse is a cleaner oracle than the hand-written ulong literals. The old test encoded the expected result as big-endian-packed ulongs, which required the reader to do the endianness reasoning by hand. Reading both input and expected through the same byte-order path makes the assertion independent of that.
  • Deterministic ramps beat my seeded-Random suggestion. input[i] = (byte)(seed + i) yields 32 distinct byte values for every one of the four seeds, and 32 distinct bytes is a complete permutation oracle — any index-mapping error is caught by a single case. Random would add non-reproducibility on failure for no extra detection power. [TestCase(0xff)] also exercises the unchecked wrap. Good call.
  • The double-swap round-trip catches half-swap errors that a symmetric input could mask, and Assert.EnterMultipleScope() has precedent in the repo (Nethermind.Xdc.Test).

EvmWord resolves via the project-wide alias in src/Nethermind/Directory.Build.props:33 (Vector256<byte>), so it's in scope in Nethermind.Core.Test with no extra using — confirmed by the passing build.

CI verification (this commit)

Check Result
Nethermind.Core.Test (ubuntu-24.04-arm) ✅ success — the AdvSimd branch runs and the assertions hold on real ARM64
Nethermind.Core.Test (ubuntu-latest) ✅ success — AVX path unregressed
Build (release, Benchmarks) / (debug, Benchmarks) ✅ success — benchmark compiles
Code formatting, Code Lint, Spell check ✅ success
Full Nethermind/Ethereum tests ✅ success, 0 failures

Remaining gate: the number

This is an optimization-only change, so the measurement is the deliverable, and it hasn't landed.

The EXPB comparison posted above is not evidence either way, for two independent reasons: it reports no cached master baseline for any payload set, and unless that runner is arm64 it exercises the AVX2/AVX-512 paths, which this PR does not touch. Don't let those tables stand in for the Graviton result.

What's needed is a two-build A/B on the same Graviton host — master (scalar fallback) vs this branch (AdvSimd) — because the branch is selected at JIT time and so a single run cannot contain both arms. Methodology detail and a caveat about the Vector256-on-arm64 stack round-trip compressing the win are in the inline comment.

Findings

Severity Finding
Medium Perf claim unmeasured. Author-acknowledged ("Target ARM64 disassembly/timing remains required before claiming a win") — but the acknowledgment defers the gate rather than clearing it. For a perf-only PR the Graviton delta has to land before merge, otherwise nothing justifies the added path.
Low Scalar fallback now has zero CI coverage. x64 legs take AVX2/AVX-512, arm64 now takes AdvSimd, so no leg reaches lines 43-48. Pre-existing on x64, but this PR closes the last door. Not worth fixing here — exposing the fallback would add surface area, which AGENTS.md discourages. Possible follow-up: a DOTNET_EnableHWIntrinsic=0 leg in Nethermind extra test variants, which would cover this and every other ISA-gated fallback in the codebase at once.
Deferring the TBL variant to a benchmark-backed follow-up rather than swapping a CI-verified sequence on my untested suggestion is the right sequencing. No objection.

Correctness, re-confirmed

The intrinsic block is byte-identical to the version I traced in the first pass; only the doc comment above it changed. REV64 (ReverseElement8 on Vector128<ulong>) reverses bytes within each 64-bit lane, ExtractVector128(x, x, 1) at ulong granularity is an 8-byte rotate — i.e. a lane swap — and Vector256.Create(lower, upper) places the reversed upper half into the result's lower half. Composition is exactly r[i] = b[31-i]. The arm64 CI leg now backs that up empirically. Guard style matches HexConverter.cs:163; all four IsSupported checks fold at JIT time, so x64 is untouched.

Security

No concerns. No new public surface, no untrusted-input handling, no allocations, no unsafe.

Verdict

The code is ready to merge as written — correct, ARM64-verified in CI, well-tested, documented, zero x64 blast radius, and every code-level comment addressed. The one thing standing between this and approval is the Graviton measurement.

Note: dotnet is not permitted in this environment, so I verified compilation and test execution through the PR's CI runs rather than locally.
• branch perf/arm64-evm-word-byteswap

}

[Benchmark(OperationsPerInvoke = 4)]
public EvmWord ByteSwap()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved ✅OperationsPerInvoke = 4 plus the serial word = word.ByteSwap() chain fixes both problems from my earlier note: the per-invoke work is now ~4 dependent shuffles (well clear of BDN's overhead floor), and the loop-carried dependency means the JIT cannot hoist the computation out of BDN's 16x unroll. The returned value is consumed, so DCE is blocked too. ByteSwap is an involution, so the chain is semantically stable. This compiles clean — Build (release, Benchmarks) and Build (debug, Benchmarks) both passed on this commit.

One methodology point before quoting a number (Low, not a code defect): this benchmark has no baseline arm, and it can't have one — the AdvSimd branch is chosen at JIT time, so a single run only ever measures whichever path the host resolves to. The A/B therefore has to be two builds on the same Graviton host: master (scalar fallback) vs this branch (AdvSimd), same BDN config.

Note that trying to get the "before" arm by disabling the ISA via env var is not a valid substitute: AdvSimd is baseline on arm64, and turning hardware intrinsics off changes codegen for GetLower()/GetUpper()/Vector256.Create as well, so the baseline would be penalised for reasons unrelated to this method and the win would come out inflated.

Worth pairing with [DisassemblyDiagnoser] on the branch run to confirm REV64/EXT actually show up in the generated loop — that also tells you how much of the win the Vector256 stack round-trip is eating, since Vector256<T> is not hardware-accelerated on arm64.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant