Skip to content

perf: experiment with SVE2 Keccak on ARM64 - #12801

Draft
kamilchodola wants to merge 2 commits into
masterfrom
perf/arm64-keccak-sve2-experiment
Draft

perf: experiment with SVE2 Keccak on ARM64#12801
kamilchodola wants to merge 2 commits into
masterfrom
perf/arm64-keccak-sve2-experiment

Conversation

@kamilchodola

@kamilchodola kamilchodola commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Changes

  • Add an experimental Linux ARM64 Keccak-f[1600] path using SVE2 SHA3 instructions (EOR3, XAR, and BCAX) with named vector locals and no vector-backed stack arrays.
  • Keep it disabled by default; enable only with exact NETHERMIND_EXPERIMENTAL_SVE2_KECCAK=1, .NET SVE2 support, Linux HWCAP2_SVESHA3, and a successful scalar-equivalence self-test.
  • Fall back safely to the existing AVX-512/scalar path when the platform probe or self-test fails.
  • Add a direct, same-process scalar/SVE permutation 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

  • dotnet test Nethermind.Core.Test/Nethermind.Core.Test.csproj -c Release --no-restore --filter FullyQualifiedName~Keccak — 1,066 passed; two expected skips on unsupported x64.
  • Fresh-process forced-opt-in fallback regression — passed on x64.
  • dotnet build Nethermind.Precompiles.Benchmark/Nethermind.Precompiles.Benchmark.csproj -c Release --no-restore — passed with zero warnings and errors.
  • Linux ARM64 correctness, generated-code inspection, and performance validation on c9gd remain required.

Documentation

Requires documentation update

  • Yes
  • No

Requires explanation in Release Notes

  • Yes
  • No

Remarks

This remains a controlled benchmark experiment. Target acceptance requires the full Keccak suite with the opt-in enabled, the direct scalar/SVE benchmark, and disassembly confirming EOR3, XAR, and BCAX without excessive spill traffic.

@claude

claude Bot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Claude finished @kamilchodola's task in 6m 13s —— View job


Review: SVE2 Keccak experiment

I hand-verified the permutation algebra and reviewed the gating, codegen shape, and benchmark. The math is correct; the problems are in verification coverage, the failure mode of the gate, and a code shape that probably can't win the benchmark it was written for.

Verdict: needs work before merge — 3 High, 3 Medium, 2 Low. Nothing is a live production risk today (the path is off by default), but the High items should land before anyone runs this on hardware.

What I verified as correct ✅

I checked the permutation lane by lane against the Keccak-f[1600] spec:

  • θ: C[x] = ⊕ a[x+5y] ✓; D[x] = C[x-1] ^ ROTL(C[x+1], 1) ✓ (d[0] = c[4] ^ ROTL(c[1],1)).
  • ρ+π: all 24 non-trivial lanes correct. XAR(Zn, Zm, #imm) = (Zn ^ Zm) ROR imm, so e.g. b[10] = XAR(a[1], d[1], 63)ROTL(a[1]^d[1], 1), matching r[1][0] = 1, and π's (x,y) → (y, 2x+3y) gives dest (0,2) → index 10 ✓. Spot-checked exhaustively: b[20]/62, b[5]/28, b[16]/36, b[2]/43, b[23]/41, b[9]/61, b[4]/14 — all match, and every d[x] operand pairs with the right column.
  • χ: BCAX(xor, value, mask) = xor ^ (value & ~mask), so a[y+3] = BCAX(b[y+3], b[y], b[y+4]) = b[y+3] ^ (b[y] & ~b[y+4]) — both wrap-around rows handled correctly ✓.
  • ι: a[0] ^= RC[round] after χ ✓.
  • AT_HWCAP2 = 26 and HWCAP2_SVESHA3 = 1<<5 match the Linux arm64 definitions ✓.
  • [DllImport("libc")] matches existing repo convention (MallocHelper, HardwareInfo) — no issue there.
  • The else if (ExperimentalSve2KeccakEnabled) branch is a static readonly bool, so the JIT folds it away on non-opted-in runs; zero cost to the existing x64/scalar hot path ✓.

Findings

# Severity Issue
1 High Static field initializer throws → cached TypeInitializationException poisons KeccakHash process-wide. Opting in on x64 doesn't fail cleanly at startup; it dies on the first hash, deep inside RLP/chain init.
2 High No correctness verification for the path. No SVE2 CI runner, no test comparing against KeccakF1600, ARM64 validation admittedly not yet run. A wrong permutation = wrong state roots. Add an enable-time self-test vs the scalar path so a mismatch becomes a startup refusal rather than a fork.
3 High Span<Vector<ulong>> forces every operand through memory. 50 vectors live vs 32 Z-registers, span elements never register-promoted, so ~85 vector ops/round become load-load-op-store. The PR's own acceptance bar ("no excessive round-loop spill traffic") is unlikely to be met. Also lands on the documented SVE restriction against Vector<T> in arrays.
4 Medium HWCAP2_SVESHA3 gates on a feature the code doesn't use. EOR3/BCAX/XAR are baseline FEAT_SVE2; FEAT_SVE2_SHA3 adds only RAX1, which isn't used (θ's D[x] is open-coded shifts). The check needlessly rejects valid SVE2 hardware and is the sole reason for the getauxval P/Invoke — deleting it removes ~10 lines and a native dep.
5 Medium Missing [SkipLocalsInit] on KeccakF1600Sve2 — 60 stackalloc vectors zeroed per call, and KeccakF runs twice for a typical 32-byte hash. KeccakF1600Avx512F already has it. (Folded into #3.)
6 Medium Benchmark can't measure the change — both benchmarks route through the same KeccakF, so A/B requires two separate runs compared by hand. A direct KeccakF1600 vs KeccakF1600Sve2 benchmark gives a same-process A/B and doubles as the missing correctness check.
7 Low 25 and 5 are repeated as literals throughout KeccakF1600Sve2; .agents/rules/coding-style.md asks for constants where a value is reused. STATE_SIZE / sizeof(ulong) already exists for the former.
8 Low The (byte) casts on the XAR rotate amounts are redundant (implicit constant conversion applies to literals) — 24 lines of avoidable noise. Also #pragma warning disable SYSLIB5003 at line 36 spans the field, method and DllImport, but only Sve2.IsSupported needs it; tighten to that line.

One design observation

Beyond the individual findings: broadcasting one scalar lane across the vector and reading back only [0] means the permutation does Vector<ulong>.Count× redundant work for 1× useful output. That's inherent to single-stream Keccak and fine in principle — the payoff is meant to come from eor3/xar/bcax collapsing 3-4 scalar ops into one. But the scalar KeccakF1600 is fully unrolled and register-resident, so the SVE2 version has to beat it while paying spill traffic (#3) and stack zeroing (#5). Worth confirming the instruction-count win is real before investing more in this shape.

Separately: FEAT_SHA3's Advanced SIMD EOR3/BCAX/XAR are far more widely available than SVE2's (Graviton3, Apple M-series) and would operate on 2 useful lanes at 128 bits. .NET doesn't currently expose them, which justifies the SVE2 route here — but it's worth noting in the experiment write-up, since it bounds how broadly a positive result could be deployed.


  • Read the diff (git diff origin/master...HEAD)
  • Verify Keccak-f[1600] permutation correctness (θ/ρ/π/χ/ι, XAR/BCAX/EOR3 semantics) — all correct
  • Check Vector<T> + SVE constraints, register pressure, allocation behaviour
  • Review the feature gate (env var, HWCAP2, static-ctor throw)
  • Review benchmark changes
  • Post findings (5 inline comments + this summary)

No code changes made — this was a review request. I did not build or run tests: the SVE2 path cannot execute on this x64 runner, and the author already reports clean Release builds for both touched projects.
• branch perf/arm64-keccak-sve2-experiment

Comment thread src/Nethermind/Nethermind.Core/Crypto/KeccakHash.std.cs Outdated
Comment thread src/Nethermind/Nethermind.Core/Crypto/KeccakHash.std.cs Outdated
Comment thread src/Nethermind/Nethermind.Core/Crypto/KeccakHash.std.cs Outdated
Comment thread src/Nethermind/Nethermind.Core/Crypto/KeccakHash.std.cs Outdated
Comment thread src/Nethermind/Nethermind.Precompiles.Benchmark/KeccakBenchmark.cs Outdated
@github-actions

Copy link
Copy Markdown
Contributor

EXPB Benchmark Comparison

Run: View workflow run

superblocks

Scenario: nethermind-flat-superblocks-perf-arm64-keccak-sve2-experiment-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) 872.55
MEDIAN (ms) 841.0
P90 (ms) 1005.2
P95 (ms) 1076.3
P99 (ms) 2768.3
MIN (ms) 619.8
MAX (ms) 2768.3
K6 TTFB
Metric PR
AVG (ms) 1470.13
MEDIAN (ms) 1016.34
P90 (ms) 2803.65
P95 (ms) 3373.91
P99 (ms) 4561.13
MIN (ms) 717.96
MAX (ms) 4781.43

realblocks

Scenario: nethermind-flat-realblocks-perf-arm64-keccak-sve2-experiment-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) 21.10
MEDIAN (ms) 17.7
P90 (ms) 34.2
P95 (ms) 41.5
P99 (ms) 88.0
MIN (ms) 0.2
MAX (ms) 185.1
K6 TTFB
Metric PR
AVG (ms) 25.34
MEDIAN (ms) 21.26
P90 (ms) 38.31
P95 (ms) 45.70
P99 (ms) 91.87
MIN (ms) 1.29
MAX (ms) 539.36

fusaka

Scenario: nethermind-flat-fusaka-perf-arm64-keccak-sve2-experiment-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.11
MEDIAN (ms) 36.0
P90 (ms) 77.2
P95 (ms) 109.9
P99 (ms) 232.4
MIN (ms) 4.7
MAX (ms) 352.0
K6 TTFB
Metric PR
AVG (ms) 56.52
MEDIAN (ms) 46.05
P90 (ms) 92.31
P95 (ms) 118.75
P99 (ms) 251.77
MIN (ms) 6.75
MAX (ms) 847.80

@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 7m 38s —— View job


Re-review: SVE2 Keccak experiment (after 0c0e722)

The rewrite is a real improvement. I re-verified the permutation from scratch after the locals restructure, and the three High items from the last pass are genuinely fixed. What's left is one new High-adjacent regression in the benchmark project, a CI-hang hazard in the new test, and the fact that the new status enum reports nothing — which matters because a silent fallback would make the experiment's own numbers misleading.

Verdict: close, but not merge-ready — 4 Medium, 4 Low, 0 Critical/High. Production risk remains ~0 (off by default, self-test-gated).

Previous findings — status

# Prior severity Status
1 High — cctor throw poisons KeccakHash Fixed. No throw path remains; Unsupported/VerificationFailed both fall back. Verified: on x64 IsSve2KeccakSupported() short-circuits at Sve2.IsSupported before the P/Invoke.
2 High — no correctness verification Fixed. Enable-time self-test over zero / all-ones / non-byte-periodic states, plus a platform-gated Sve2_permutation_matches_scalar test and the fresh-process fallback regression.
3 High — Span<Vector<ulong>> through memory Addressed. All 60 vectors are named locals now, [SkipLocalsInit] present, no vector stackalloc. The Vector<T>-in-array hazard is gone entirely.
5 Medium — missing [SkipLocalsInit] ✅ Fixed.
6 Medium — benchmark can't A/B Fixed. KeccakPermutationBenchmark calls both permutations directly, verifies equality in setup, InProcessNoEmitToolchain + AlwaysUseLocal gives a same-process comparison. The API-level ComputeHash benchmark was correctly dropped.
4 Medium — HWCAP2_SVESHA3 gate 🟡 Acknowledged with rationale — not blocking. See the RAX1 comment: keeping the gate is the conservative direction, but then the code should use the instruction the gate guarantees.
7, 8 Low — magic 25, redundant (byte) casts, pragma scope ⬜ Not addressed.

Correctness — re-verified, exact ✅

The new version is a line-for-line transliteration of KeccakF1600Scalar, which makes it checkable mechanically rather than by algebra. I did both:

  • All 48 XAR immediates are 64 − scalar ROL amount, in both half-rounds. Spot list: 44→20, 43→21, 21→43, 14→50, 28→36, 3→61, 45→19, 61→3, 1→63, 6→58, 25→39, 8→56, 18→46, 27→37, 36→28, 10→54, 15→49, 56→8, 62→2, 55→9, 39→25, 41→23, 2→62 — all correct, and the odd-round e* block repeats them identically.
  • Operands and assignment targets match the scalar version lane for lane, including the deliberately reordered bCi/bCe/bCa sequence at the start of the odd half-round.
  • Sve2.Xor(Xor(a,b,c),d,e) = the 5-way column XOR ✓. BitwiseClearXor(bCa, bCi, bCe) = bCa ^ (bCi & ~bCe) = scalar bCa ^ ((~bCe) & bCi) ✓ for all 50 sites. ι applied to eba/aba with round / round + 1 ✓.
  • VL-agnostic: broadcast in, lane 0 out, no cross-lane ops — correct for any SVE vector length.
  • KeccakF1600KeccakF1600Scalar rename has no dangling references anywhere in the repo ✓.

Findings

# Severity Issue
1 Medium GlobalSetup throws on every non-SVE2 host → the README's documented --filter "*Benchmark*" run of the precompiles assembly now produces a failed benchmark on x64. Verified against Nethermind.Benchmark.Runner/Program.cs:78-82. Use a BDN IFilter to decline selection instead of throwing.
2 Medium Child-process pipe deadlock + no timeout in Experimental_sve2_opt_in_on_unsupported_host_does_not_poison_KeccakHash. Sequential ReadToEnd() on stdout then stderr with both redirected; a chatty test host fills the stderr buffer and both sides block, then bare WaitForExit() turns it into a job-timeout hang rather than a failure.
3 Medium The status enum reports nothing and both catches swallow silently. Unsupported and VerificationFailed are indistinguishable from Disabled at runtime. If the opt-in silently falls back on c9gd, the EXPB run reads as "SVE2 gives no speedup" when SVE2 never executed — a wrong conclusion from a healthy node. Also flags the load-bearing RoundConstants-before-ExperimentalSve2Keccak declaration order, which nothing documents.
4 Medium RAX1 is unused despite the SHA3 gate. RotateLeftOne + Xor is 4 vector ops × 5 lanes × 2 half-rounds × 24 = 960 ops/permutation that RAX1 does in 240 — roughly 19% of the permutation's instruction count, and the gate already guarantees the instruction is present.
5 Low 48 redundant (byte) casts on the XAR immediates (implicit constant conversion covers literals); #pragma warning disable SYSLIB5003 at line 64 spans the DllImport too — only Sve2.IsSupported needs it.
6 Low Literal 25 in VerifySve2Keccak and KeccakTests; STATE_SIZE / sizeof(ulong) already exists (.agents/rules/coding-style.md — constants over reused magic numbers).
7 Low The fresh-process test spawns a full test host on every Nethermind.Core.Test run on x64, and passes on exit code alone — if the --filter ever stops matching, coverage vanishes silently while still green.
8 Low [assembly: InternalsVisibleTo("Nethermind.Precompiles.Benchmark")] is new surface for a benchmark's sake. Acceptable, but it means Nethermind.Core internals are now reachable from a benchmark project by default — worth a moment's thought before it becomes a habit.

Performance outlook — still unresolved, and this is the point of the PR

The named-locals rewrite removed the guaranteed memory round-trip, but it does not remove spilling: 60 live Vector<ulong> locals against 32 Z-registers means the allocator must spill, and SVE spill slots are VL-sized (str z/ldr z), so on a 256-bit-VL part each spill moves 2× the bytes the scalar version's 64-bit spills do. The scalar path is the same 60-local shape against 31 X-registers and spills too — so the comparison is "wide spills + ~19% fewer arithmetic ops (more if RAX1 lands)" versus "narrow spills + more ops."

On top of that, per KeccakF call the SVE path pays 25 broadcasts on entry and 25 lane-0 extracts on exit, and KeccakF runs twice for a typical 32-byte hash. And the fundamental ceiling stands: broadcasting one lane and reading back [0] does Vector<ulong>.Count× redundant work for 1× useful output.

None of that is a defect — it's why the disassembly step you've still got open is the real gate. I'd suggest landing RAX1 first so the measurement reflects the best version of this shape.

What I could not check

dotnet is not permitted on this runner, so I neither built nor ran tests. In particular I could not confirm that Sve2.Xor(3-arg), Sve2.XorRotateRight and Sve2.BitwiseClearXor have exactly these signatures in .NET 10 — I'm relying on your reported clean Release build for both touched projects. The SVE2 path also cannot execute here, so the two new platform-gated tests self-Ignore.


  • Read the updated diff (git diff origin/master...HEAD)
  • Re-verify the permutation after the locals rewrite — all 48 rotates + 50 BCAX sites + ι confirmed against the scalar path
  • Check the new self-test, status enum, and fallback gate
  • Review the new tests and permutation benchmark
  • Post findings (4 inline comments + this summary)

No code changes made — this was a review request.
• branch perf/arm64-keccak-sve2-experiment

public void Setup()
{
if (!KeccakHash.IsSve2KeccakSupported())
throw new PlatformNotSupportedException("KeccakPermutationBenchmark requires SVE2 SHA3 support.");

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.

Medium — throwing from [GlobalSetup] breaks the documented "run all precompile benchmarks" command on every non-SVE2 host.

Nethermind.Benchmark.Runner runs BenchmarkSwitcher.FromAssemblies(...).Run(benchmarkArgs, ...) over an assembly list that includes typeof(KeccakBenchmark).Assembly (Program.cs:78-82), and this project's README documents:

dotnet run -c Release --project Nethermind.Benchmark.Runner -- --filter "*Benchmark*"

*Benchmark* matches KeccakPermutationBenchmark, so on x64 — i.e. every dev box and CI runner today — this class is selected and its GlobalSetup throws. BDN turns that into a failed benchmark entry plus error output in the middle of an otherwise-clean precompiles run. The class is unrunnable there by design; it should decline to be selected rather than fail.

Cleanest fix that keeps the class discoverable on the target: add an IFilter to InProcessConfig that returns false when KeccakHash.IsSve2KeccakSupported() is false, so BDN excludes the benchmarks instead of erroring. Keep the hard throw only for the state-mismatch check below — that one should fail loudly.

Smaller note on the same method: _scalarState and _sveState are permuted once in Setup and then keep being permuted in place across iterations, so the two benchmarks run over diverging state. Keccak-f timing is data-independent, so this doesn't bias the A/B — worth a one-line comment saying so, since it looks like a bug otherwise.

Fix this →

Comment on lines +224 to +226
string standardOutput = process.StandardOutput.ReadToEnd();
string standardError = process.StandardError.ReadToEnd();
process.WaitForExit();

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.

Medium — this is the classic Process pipe deadlock, and there's no timeout to break out of it.

Both StandardOutput and StandardError are redirected, and they're drained sequentially: the parent blocks in StandardOutput.ReadToEnd() until the child closes stdout. If the child writes more than the stderr pipe buffer (~64 KB on Linux) before finishing, the child blocks writing stderr, the parent blocks reading stdout, and neither ever proceeds. Then WaitForExit() with no timeout means the hang lasts until the GitHub Actions job timeout rather than failing the test.

A test host is exactly the kind of child that can produce bulk stderr — MSBuild/MTP diagnostics, a runtime warning, an AssemblyLoadContext complaint. It'll pass locally and hang CI on the one run where it matters.

using Process process = Process.Start(startInfo)
    ?? throw new InvalidOperationException("Could not start the Keccak child test process.");

Task<string> standardOutputTask = process.StandardOutput.ReadToEndAsync();
Task<string> standardErrorTask = process.StandardError.ReadToEndAsync();

if (!process.WaitForExit(ChildProcessTimeoutMs))
{
    process.Kill(entireProcessTree: true);
    Assert.Fail("The Keccak child test process did not exit in time.");
}

string standardOutput = standardOutputTask.GetAwaiter().GetResult();
string standardError = standardErrorTask.GetAwaiter().GetResult();

Related (Low): the assertion is ExitCode == 0 only, so if the --filter ever stops matching Experimental_sve2_opt_in_child_computes_known_hash — a rename, an MTP option change — the regression silently stops covering anything while still passing. Assert on something proving the test actually executed (parse the TRX in resultsDirectory, or check the child output for one passed test).

Fix this →

Comment on lines +45 to +78
private static readonly ExperimentalSve2KeccakStatus ExperimentalSve2Keccak = GetExperimentalSve2KeccakStatus();

private static ExperimentalSve2KeccakStatus GetExperimentalSve2KeccakStatus()
{
try
{
if (Environment.GetEnvironmentVariable("NETHERMIND_EXPERIMENTAL_SVE2_KECCAK") != "1")
return ExperimentalSve2KeccakStatus.Disabled;

if (!IsSve2KeccakSupported())
return ExperimentalSve2KeccakStatus.Unsupported;

return VerifySve2Keccak() ? ExperimentalSve2KeccakStatus.Enabled : ExperimentalSve2KeccakStatus.VerificationFailed;
}
catch (Exception)
{
return ExperimentalSve2KeccakStatus.VerificationFailed;
}
}

#pragma warning disable SYSLIB5003
internal static bool IsSve2KeccakSupported()
{
try
{
return OperatingSystem.IsLinux()
&& RuntimeInformation.ProcessArchitecture == Architecture.Arm64
&& Sve2.IsSupported
&& (GetAuxiliaryValue(AT_HWCAP2) & HWCAP2_SVESHA3) != 0;
}
catch (Exception)
{
return false;
}

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.

Medium — the four-state status enum is computed and then thrown away, and both catch blocks swallow the exception with no trace. A silent fallback will corrupt the experiment it exists to serve.

The static-initializer poisoning problem is properly fixed — thanks. But the only consumer is ExperimentalSve2Keccak == ExperimentalSve2KeccakStatus.Enabled at line 87, so Unsupported and VerificationFailed are indistinguishable from Disabled at runtime, and nothing reports them. Concretely: an operator sets NETHERMIND_EXPERIMENTAL_SVE2_KECCAK=1 on the c9gd box, the HWCAP2 bit isn't set (see the SVE2-vs-SVE2-SHA3 point below — that's the likely way to land here) or the self-test trips, and the node silently runs the scalar path. The EXPB numbers then get read as "SVE2 Keccak gives no speedup" when SVE2 Keccak never ran. That's a wrong conclusion drawn from a working node — the worst outcome for a benchmark experiment.

.agents/rules/robustness.md also asks for at minimum a log on a swallowed exception; catch (Exception) { return VerificationFailed; } discards the reason entirely, so a self-test failure and a missing getauxval look identical.

Minimal fix that stays inside Nethermind.Core (no logger available here): keep the exception in a field and expose the status, then have a startup step log it.

internal static ExperimentalSve2KeccakStatus Sve2KeccakStatus => ExperimentalSve2Keccak;
internal static Exception? Sve2KeccakFailure { get; private set; }

Even a one-off Console.Error.WriteLine on Unsupported/VerificationFailed when the opt-in was explicitly requested would be enough for an experiment. If you'd rather not surface it at all, then the enum is dead differentiation and should collapse back to a bool.

Two smaller points on this block:

  • Static-initializer ordering is a load-bearing invariant with nothing marking it. VerifySve2Keccak calls KeccakF1600Scalar, which reads RoundConstants. That works only because RoundConstants (line 24) is declared textually before ExperimentalSve2Keccak (line 45) in the same file — field initializers run in declaration order. Move either field to KeccakHash.cs, or reorder them, and the self-test dereferences a null array, the catch converts it to VerificationFailed, and the feature silently never enables. Worth one comment on line 45 stating the dependency.
  • The #pragma warning disable SYSLIB5003 at line 64 spans IsSve2KeccakSupported and the getauxval DllImport; only the Sve2.IsSupported reference needs it.

Fix this →

Comment on lines +394 to +398
da = Vector.Xor(bCu, RotateLeftOne(bCe));
de = Vector.Xor(bCa, RotateLeftOne(bCi));
di = Vector.Xor(bCe, RotateLeftOne(bCo));
@do = Vector.Xor(bCi, RotateLeftOne(bCu));
du = Vector.Xor(bCo, RotateLeftOne(bCa));

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.

Medium (performance) — you're gating on HWCAP2_SVESHA3 and then not using the one instruction it guarantees. RAX1 collapses these five lines from 20 vector ops to 5.

RAX1 is defined as Zd = Zn EOR ROL(Zm, 1) — which is exactly da = bCu ^ ROL(bCe, 1). Right now each of the five θ D[x] values costs ShiftLeft + ShiftRightLogical + BitwiseOr + Xor = 4 vector ops, so 20 per half-round, 40 per loop body, 960 per permutation. With RAX1 that's 5 / 10 / 240.

Rough per-half-round vector op count for this implementation is ~80 (10 for the five 5-way EOR3 column XORs, 20 for D[x], 25 XAR, 25 BCAX), so this is ~19% of the whole permutation's instruction count — material for an experiment that has to beat a fully-unrolled register-resident scalar path.

It also makes the gating self-consistent. Since you're keeping the HWCAP2_SVESHA3 requirement (fine by me — it's the conservative direction and you've given your rationale), RAX1 is guaranteed present whenever this code runs, so there is no portability argument for open-coding it. As written, the gate rejects hardware over an instruction the code never issues.

One factual correction for the record, since it affects who can run the experiment: in the Arm ARM, SVE EOR3, BCAX and XAR are baseline FEAT_SVE2; FEAT_SVE2_SHA3 adds only RAX1. GCC's sve2-sha3 flag enables the extension, but the feature table listing those mnemonics together is about what the flag permits, not about what each instruction requires. Practical consequence: on an SVE2 part without the SHA3 crypto extension the path silently falls back with no log (see my other comment) — so if the c9gd numbers come back flat, check the status before concluding anything. Using RAX1 makes this moot.

Fix this →

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