Skip to content

SIMD kernel throughput optimization: streaming ChaCha/Poly1305, fused AES AEAD rework, and bulk GHASH AAD#145

Merged
Xor-el merged 4 commits into
masterfrom
perf/simd-kernel-throughput-optimization
Jul 11, 2026
Merged

SIMD kernel throughput optimization: streaming ChaCha/Poly1305, fused AES AEAD rework, and bulk GHASH AAD#145
Xor-el merged 4 commits into
masterfrom
perf/simd-kernel-throughput-optimization

Conversation

@Xor-el

@Xor-el Xor-el commented Jul 11, 2026

Copy link
Copy Markdown
Owner

Summary

This PR re-architects the hottest x86 SIMD paths for ChaCha20-Poly1305, AES AEAD modes, and standalone GHASH, and adds same-key re-Init fast paths so per-message nonce rotation no longer pays full key setup.
ChaCha20 / Poly1305 streaming kernels

  • Vertical ChaCha kernels (AVX2 8-way, SSE2/SSSE3 4-way) now stream AGroups groups per call on a 64B-aligned frame: one-time prologue, light per-group state reload, resident c-pair rounds, and a fused add-back/transpose/xor tail written straight to output (no bounce buffer)
  • New i386 AVX2 8-way kernel (stack-resident state rows, resident rot16/rot8 masks) roughly doubles 32-bit throughput
  • Engine bulk ladder: one WideGroupsSafe wrap guard + TryProcessWide per tier; each wide kernel is entered once per span instead of per group
  • Poly1305 AVX2 bulk loop is modulo-scheduled on x86_64; i386 AVX2 and both SSE2 kernels use 64B-aligned frames, k-major multiply, and in-frame aligned table copies
  • TChaCha20Poly1305 bulk data now runs one whole-span cipher call and one whole-span MAC call per ProcessBytes; 512-byte stride loops are gone
    AES AEAD mode fast paths (same-key re-Init)
  • GCM: skip key schedule, hash subkey, multiplier, H-power table rebuild, and kernel re-acquire on same-key re-Init
  • OCB: skip both key schedules, L-table rebuild, and kernel re-acquire when the key is unchanged (keeps Ktop cache; direction-aware)
  • CCM: cache the CTR wrapper and re-init IV-only per packet; decrypt writes straight into the caller buffer and zero-wipes on tag failure (no scratch bounce)
    Fused AES kernel rework
  • CTR (both arches): counter lives in registers native-endian across the span; removes per-block counter memory RMW and store-forward stalls
  • OCB (both arches): round-interleaved, double-buffered offset ladder; i386 6-wide (min 12 blocks), x86_64 8-wide (min 16 blocks)
  • GCM (both arches): kernel owns an H^8..H^1 table pre-multiplied by x in reflected representation; two carry-less multiplies per batch instead of a long shift/XOR ladder; running GHASH state stays reflected across batches
  • GCM x86_64: loop-carried state in registers, round keys as aligned memory operands; counter blocks staged ahead through GPR stack slots (3–7% gain; avoids store-forward failure on naive store-then-load)
  • Karatsuba GHASH variant benchmarked and rejected (neutral on x86_64, 4–7% regression on i386)
    Standalone GHASH + bulk AAD
  • 4-/8-way GcmGhashFull batch kernels now loop over ABatchCount inside asm (setup paid once per call, not per 64/128-byte chunk)
  • TGcmUtilities.InitEightWayHPowFromH emits every H power pre-multiplied by x (DivideP) for the folding reduction
  • ProcessAadBytes hashes whole 128-byte spans in a single kernel call; remainder falls back to per-block loop (AAD was ~110 MB/s via one-block-at-a-time dispatch)
  • Table/kernel changes are consistent across GCM 4-/8-way paths, new bulk-AAD path, GCM-SIV POLYVAL Horner batch, and fused CTR+GHASH kernel

Xor-el added 4 commits July 11, 2026 05:28
ChaCha (x86_64 + i386):
- Vertical kernels (AVX2 8-way, SSE2/SSSE3 4-way) now stream AGroups
  groups per call on a 64B-aligned frame: one-time prologue, light
  per-group state reload, resident c-pair rounds (2 c-stores + 2 c-loads
  per round), and a fused add-back/transpose/xor tail written straight
  to the output (no bounce buffer).
- New i386 AVX2 8-way kernel (stack-resident state rows, resident
  rot16/rot8 masks) roughly doubles 32-bit throughput.
- Engine bulk ladder: one WideGroupsSafe wrap guard + TryProcessWide per
  tier; each wide kernel is entered once per span instead of per group.

Poly1305:
- x86_64 AVX2 bulk loop is modulo-scheduled: r-power rows load into the
  freed temps while pending limbs absorb, h2 (folded early in the prior
  reduce) opens all five accumulators, and the next block's load + limb
  split thread through the multiply and lazy reduce.
- i386 AVX2 and both SSE2 kernels: 64B-aligned frames, k-major multiply
  with the accumulator resident across its terms, mask26/padbit as
  memory operands (keeps H0..H4 resident on i386), and a one-time
  aligned in-frame table copy so SSE2 multiplies take memory operands.

ChaCha20Poly1305:
- Bulk data now runs one whole-span cipher call and one whole-span MAC
  call per ProcessBytes; the 512-byte stride loops are gone.
…M subsystem

Mode-level: every AEAD mode now has a same-key re-Init fast path, so a fresh
nonce per message no longer pays for key-dependent setup:

- OCB: skip both key schedules, the L-table rebuild and the kernel re-acquire
  when the key is unchanged (keeps the Ktop cache; direction-aware).
- CCM: cache the CTR wrapper and re-init it per packet with IV-only
  parameters; same-key Init skips the engine init and kernel re-acquire.
  Decrypt now writes straight into the caller's buffer and zero-wipes it
  before raising on a tag failure, instead of bouncing the whole plaintext
  through a scratch copy; the no-unverified-plaintext guard test asserts the
  new wipe contract.
- GCM: skip the key schedule, hash subkey, multiplier and H-power table
  rebuild and the kernel re-acquire on same-key re-Init (the CTR keystream is
  direction-independent, so key equality alone gates it).

Kernel-level:

- CTR (both arches): the counter staging no longer read-modify-writes counter
  memory per block. The counter lives in registers native-endian across the
  span (the constant prefix is store-only), with carry-outs rare-pathed;
  this removes a store-forward stall per staged block.
- OCB (both arches): round-interleaved, double-buffered offset ladder - each
  iteration's AES rounds stage the next iteration's deltas into the opposite
  ping-pong bank, so the ladder's serial bsf/load/xor chain rides the AES
  latency instead of running as a prefix. i386 is 6-wide (min 12 blocks),
  x86_64 8-wide (min 16) with a branch-free 64-bit counter and key
  memory-operand rounds.
- GCM (both arches): the kernel now owns an H^8..H^1 table pre-multiplied by
  x in the reflected representation, so the per-batch 256-bit product reduces
  with two carry-less multiplies by the folded field constant instead of a
  long shift/XOR ladder, and the running GHASH state stays reflected across
  batches (byte-reversal only at kernel entry/exit). x86_64 additionally
  holds all loop-carried state in registers (no per-batch context memory
  traffic) and feeds round keys as aligned memory operands. i386 gets a
  16-aligned frame: aligned accumulator slots, a resident byte-reverse mask
  used as a shuffle memory operand, in-register cross-product combining and
  an in-register counter.
Replace the per-block shuffle-port register inserts in the fused GCM
kernel's counter build with aligned stack slots: the fixed 12-byte J0
prefix is staged once in the prologue, and each batch's AES-only round 9
stages the NEXT batch's four-byte counter words, so the loop-top movdqa
loads never read a freshly written slot (a naive store-then-load form
forwards-fails on the partial overlap and serializes the loop top - it
measured 40% slower). The prologue pre-stages batch 0; the final batch
over-stages into scratch.

This removes sixteen shuffle-port uops per 128-byte batch and is worth
3-7% on x86-64. The i386 kernel keeps its insert-based build: the staged
form measured neutral-to-negative there, with both placements tried.

A Karatsuba variant of the batch GHASH (three carry-less multiplies per
block plus a precomputed halves-XOR table column) was also implemented,
verified and benchmarked: it is throughput-neutral on x86-64 and a 4-7%
regression on i386 - the saved multiply is repaid on the same execution
port by the added shuffle, plus an extra table load per iteration - so
the schoolbook four-multiply form stays.
…ulk path

The 4-/8-way GHASH batch kernels (GcmGhashFull) previously paid their
whole setup on every 64/128-byte call: a ten-register nonvolatile
save/restore, the byte-reversal round-trip of the running state, a long
shift/XOR reduction and a Pascal call boundary. Worse, the GCM mode's
AAD path never used them at all - ProcessAadBytes fed one block at a
time through the interface-dispatched multiplier, running at ~110 MB/s.

Kernels: the batch body now loops over an ABatchCount parameter inside
the asm, so the save/restore, mask load, reduction constant and state
byte-reversal are paid once per call; the running state stays reflected
across batches (a register on x86-64, an aligned frame slot on i386,
which also serves the byte-reverse mask as shuffle memory operands).
The per-batch reduction is two carry-less multiplies by the folded
field constant instead of the shift ladder.

Table: TGcmUtilities.InitEightWayHPowFromH now emits every H power
pre-multiplied by x (DivideP), which is what the folding reduction
requires. Every consumer funnels through the same table and kernel
body - the GCM 4-/8-way paths, the new bulk-AAD path, the GCM-SIV
POLYVAL Horner batch and the fused CTR+GHASH kernel (whose constructor
now takes a plain copy instead of shifting its own) - so the change is
consistent across the board. The mode's post-dispatch scalar fallbacks
(unreachable in practice: they share their gate with the table
precompute) were rewritten as per-block multiplier folds, keeping them
correct independent of the table layout.

Dispatch: TryFusedFour/EightShuffledGhash and the POLYVAL batch chain
gained the batch count; ProcessAadBytes hashes whole 128-byte spans in
a single kernel call and leaves the remainder to the per-block loop.
@Xor-el Xor-el merged commit 413efb1 into master Jul 11, 2026
24 checks passed
@Xor-el Xor-el deleted the perf/simd-kernel-throughput-optimization branch July 12, 2026 00:52
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.

1 participant