Skip to content

Vertical ChaCha/Poly1305 SIMD kernels, IBulkStreamCipher, and unified bulk ProcessBlocks(count) APIs#141

Merged
Xor-el merged 6 commits into
masterfrom
perf/chacha-poly1305-simd
Jul 9, 2026
Merged

Vertical ChaCha/Poly1305 SIMD kernels, IBulkStreamCipher, and unified bulk ProcessBlocks(count) APIs#141
Xor-el merged 6 commits into
masterfrom
perf/chacha-poly1305-simd

Conversation

@Xor-el

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

Copy link
Copy Markdown
Owner

Summary

This PR overhauls x86 ChaCha and Poly1305 SIMD paths, introduces a bulk stream-cipher capability interface, and unifies fixed-width bulk APIs across stream and block engines.

ChaCha SIMD + shared engine core

  • Replace row-layout AVX2 kernels with vertical (word-per-lane) kernels: SSE2/SSSE3 4-way (256B) and AVX2 8-way (512B), on x86_64 and i386; retire the old row kernels
  • Add TChaChaBaseEngine as the shared ChaCha core (block function + 512/256/128/64B dispatch ladder); TChaCha7539Engine and legacy DJB TChaChaEngine derive from it and differ only in counter width
  • One CHACHA_CTR64-parameterized kernel serves both variants; DJB advances the 64-bit counter with a WideBlocksSafe guard near the low-word wrap
  • Hoist AVX2 8-way rotate masks out of per-quarter-round stack loads (~120 fewer load micro-ops per 512-byte block)

IBulkStreamCipher + ChaCha20-Poly1305 AEAD

  • Add ClpIBulkStreamCipher — the stream analogue of IBulkBlockCipher; resolved via Supports() and driven through ProcessBlocks(count)
  • TSalsa20Engine and TChaChaBaseEngine implement ProcessBlocks(count) via a virtual DoProcessBlocks ladder (ChaCha 8→4→2→1, Salsa 2→1) with pointer-form *Fast inner helpers
  • TChaCha20Poly1305 resolves bulk capability and processes the AEAD body in 512-byte (8-way) strides instead of the interface-capped 256B path

Poly1305 SIMD

  • Add 2-way SSE2 bulk kernel (x86_64 + i386), filling the sub-AVX2 gap that previously fell back to radix-2^26 scalar
  • Restructure AVX2 bulk carry-reduce on x86_64 into two interleaved chains (~+2.5% MAC / ~+2% AEAD); i386 keeps serial reduce (two-chain was 4–12% slower due to register pressure)
  • Rewrite AVX2 kernels with canonical minimal-displacement VEX encodings (~20% fewer bytes)

API cleanup — width-agnostic bulk

  • Fold ChaCha/Salsa ProcessBlocks2/4/8 into ProcessBlocks(count); engines compute whole-block count and ladder internally
  • Drop ProcessBlocks2/4 from IChaCha7539Engine; bulk is purely the optional IBulkStreamCipher capability
  • Fold AES ProcessFourBlocks / ProcessEightBlocks into ProcessBlocks(count) on IAesHardwareEngine; remove unused fixed-width staging from TAesEngineX86

Tests

  • Add TStreamCipherTestBase (engine-factory + label hooks) with reusable whole-vs-chunked, ProcessBytes vs ReturnByte, and bulk-vs-single-block checks
  • Reparent Salsa20 / XSalsa20 / ChaCha (DJB) / ChaCha7539 / XChaCha20 suites onto it; drop duplicated per-suite harnesses and ChaCha7539ProcessBlocks2Tests
  • Migrate AES hardware batch coverage to ProcessBlocks(4) / ProcessBlocks(8)

Xor-el added 6 commits July 9, 2026 14:45
…tream tests

Kernels / engines:
- Replace the row-layout ChaCha AVX2 kernels with vertical (word-per-lane)
  kernels: SSE2/SSSE3 4-way (256B) and AVX2 8-way (512B), on x86_64 and i386;
  retire the old row kernels.
- Add TChaChaBaseEngine as the shared ChaCha core (block function + the
  512/256/128/64B dispatch ladder). TChaCha7539Engine and the legacy DJB
  TChaChaEngine derive from it and differ only in counter width: one
  CHACHA_CTR64-parameterized kernel serves both variants (DJB advances the
  64-bit counter; a WideBlocksSafe guard drops a tier near the low-word wrap).

IBulkStreamCipher:
- Add ClpIBulkStreamCipher, the stream analog of IBulkBlockCipher.
  TSalsa20Engine and TChaChaBaseEngine implement ProcessBlocks(count) via a
  virtual DoProcessBlocks ladder (Salsa 2->1, ChaCha 8->4->2->1) with
  pointer-form *Fast inner helpers.
- TChaCha20Poly1305 resolves it and processes the AEAD body 512 bytes (8-way)
  per call instead of the interface-capped 256, keeping the same running-count
  bookkeeping. ~40% AEAD throughput on large buffers (16MB: 690 -> 982 MB/s).

Tests:
- Add TStreamCipherTestBase (engine-factory + label hooks, AES-test pattern)
  with reusable whole-vs-chunked, ProcessBytes-vs-ReturnByte and
  bulk-vs-single-block checks. Reparent the Salsa20 / XSalsa20 / ChaCha (DJB) /
  ChaCha7539 / XChaCha20 suites onto it, keeping their KAT vectors and dropping
  the duplicated per-suite harnesses.
Now that IBulkStreamCipher.ProcessBlocks(count) exists, the fixed-width bulk
methods are redundant with the unified DoProcessBlocks ladder.

- Engines: TChaChaBaseEngine and TSalsa20Engine ProcessBytes/DoFinal compute the
  whole-block count and make a single DoProcessBlocks call (which ladders
  8->4->2->1 / 2->1 internally) plus the partial tail, replacing the fixed-width
  tier cascades (and their per-tier re-validation). Remove the public
  ProcessBlocks2/4/8 (ChaCha) and ProcessBlocks2 (Salsa).
- Interface: drop ProcessBlocks2/4 from IChaCha7539Engine (it now exposes only
  DoFinal + ProcessBlock); bulk is purely the optional IBulkStreamCipher
  capability. Engine bulk surface is {ProcessBlock, ProcessBlocks(count)}.
- TChaCha20Poly1305: collapse the ProcessBlock/2/4/8 wrappers into one
  ProcessBlocksBulk(count) and drive the AEAD body through IBulkStreamCipher as a
  512B (8-way) stride loop + a single remaining-blocks call, keeping the same
  FDataCount bookkeeping. A single-block else-path covers a custom engine that
  exposes no bulk interface.
…sBlocks(count)

The fixed-width batch methods on IAesHardwareEngine had no production caller (all
modes use IBulkBlockCipher.ProcessBlocks(count); the fused AES-NI kernels use the
raw key schedule) and baked x86's 4/8 widths into what is meant to be an
architecture-neutral hardware-AES interface (a future ARMv8 engine seats on the
same interface).

- IAesHardwareEngine: drop ProcessFourBlocks / ProcessEightBlocks (array + ptr);
  it now adds only the single-block ProcessBlock(ptr) over IBulkBlockCipher. Bulk
  is the width-agnostic ProcessBlocks(count) ladder each engine drives at its own
  internal widths.
- TAesEngineX86: remove the public fixed-width methods and their partial-overlap
  stack-buffer staging (no caller; it contradicts the identical-or-disjoint
  IBulkBlockCipher contract). The 4/8-wide *Fast helpers stay as the strict-private
  inner steps of the ProcessBlocks ladder; single-block ProcessBlock keeps its
  overlap staging.
- AesHardwareEngineTests: migrate batch-vs-single and PByte-parity coverage to
  ProcessBlocks(4)/ProcessBlocks(8) (arch-neutral, ready for a future ARM engine);
  drop the 6 partial-overlap memory-layout tests that exercised the removed staging.
Fills the sub-AVX2 gap in the Poly1305 dispatch ladder: without it, any
non-AVX2 CPU fell back to the radix-2^26 scalar path, collapsing the MAC
(~240 MB/s) and Poly-bound AEAD (~70 MB/s). The new kernel is a 2-lane
(xmm) radix-2^26 narrowing of the 4-way AVX2 kernel — same 5x5 schoolbook
multiply mod 2^130-5 with the 5*r wraparound, 2 blocks/iter against an
r/r^2 power table, pshufd horizontal fold — keeping SIMD/scalar radix
parity across the whole ladder.
- x86_64: the bulk-loop carry-reduce is restructured into two interleaved
  chains (low D0->D1->D2->D3->H4, high D3->D4->H0->H1) to shorten the serial
  critical path. Measured ~+2.5% MAC / ~+2% AEAD; the run-once tail and final
  reduces stay serial.
- i386: the reduce stays serial. The two-chain schedule was benched ~4-12%
  SLOWER on i386 -- its 8-ymm register file (D0..D4 plus an H backup spill to
  a 320-byte esp frame, and the multiply spills each accumulator per term) is
  throughput-bound, and the second carry temp would clobber padbit, forcing a
  rebuild each iteration. Both arches emit identical MACs.

Both AVX2 kernels are re-emitted with canonical minimal-displacement VEX
encodings (~20% fewer bytes than before) and plain mnemonic comments.
Verified by KAT on both arches plus a displacement-insensitive byte diff
against the previous kernels: i386 matches instruction-for-instruction;
x86_64 differs only in the reduce and in disp widths.
The rot16/rot8 rotations used vpshufb ymm, ymm, [rsp+mask], reloading the
shuffle control from the stack on every quarter-round - ~160 mask loads per
512-byte block, all landing on the load ports that the c-word spills already
contend for.

At each rotate step the quarter-round temp registers are free, so load the
mask into a temp once per rotate phase and use vpshufb ymm, ymm, ymm for all
four lanes. The pshufb count (and its port pressure) is unchanged, but mask
loads drop to ~40 per 512-byte block - a strict reduction of ~120 load
micro-ops per block, relieving the load ports the c-spills bottleneck on.
@Xor-el Xor-el merged commit 46bb034 into master Jul 9, 2026
24 checks passed
@Xor-el Xor-el deleted the perf/chacha-poly1305-simd branch July 9, 2026 23:09
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