Skip to content

Benchmarks only measure the backend the runner's CPU picks, so fallback optimizations (e.g. #142) are invisible #161

Description

@reaperhulk

Problem

#142 speeds up the scalar x86-64 GHASH (the PR body says scalar AES-GCM goes from 50 to 109 MiB/s on 16 KiB). Its Benchmarks run (job) shows no change:

Benchmark Base Head Change
aes-128-gcm-encrypt/16384 4.24 µs 4.25 µs 0.3% slower
aes-128-gcm-stream/16384 4.30 µs 4.27 µs 0.6% faster

4.25 µs for 16 KiB is about 3.6 GiB/s, which is the AES-NI + PCLMULQDQ path from #137. The scalar path at 109 MiB/s would take about 140 µs.

Cause: AesGcm::new (src/aes_gcm.rs) picks the backend at run time, and GitHub's x86-64 runners have AES-NI, PCLMULQDQ and SSSE3. So every benchmark runs vg_ghash_pclmul, and the vg_ghash that #142 changes never runs. The comparison is correct (nothing it measured changed), but it measures the wrong code.

The same blind spot already covers SHA-256 and HMAC-SHA-256 on x86-64 (the SHA-NI dispatch in src/hashes/sha256.rs). It will cover every future SIMD or crypto-extension backend: the fallbacks those leave behind get no performance signal at all, and a regression in them would not fail the check either.

Two smaller problems make bulk-cipher results harder to read:

  • The summary shows times, not throughput. For a bulk cipher, "4.25 µs" means little without doing the arithmetic. The groups already declare Throughput::Bytes, so MiB/s is free to report.
  • Every iteration includes key setup (key expansion plus computing H for GCM). That's the right default ("one complete operation", bench/benches/primitives/main.rs), but at 64 and 1024 bytes setup dominates, and there is no measurement of steady-state per-byte cost.

Plan

The aim is a benchmark of every backend the runner can execute, without making the per-PR Benchmarks job noticeably slower. #142's x86-64 benchmark step took 70 s.

  1. Benchmark each backend separately. The API already has __with_features(mask) for tests (AesGcm, AesGcmStream, the hash macro in src/hashes/mod.rs, Hmac). Add a backend to the benchmark id, e.g. aes-128-gcm-encrypt/verified-garbage@scalar/16384 and …@aesni/16384. Skip a backend whose features the runner lacks (cpu::available on the mask), and print that it was skipped rather than silently falling back. bench_compare.py only needs its id parsing extended.
  2. Keep the extra backends cheap on PRs. Non-default backends run only the encrypt (or digest) group at 16 KiB, not every group at every size. That adds about 4 benchmarks (AES-GCM and SHA-256 scalar, plus HMAC later), roughly 4 × 0.7 s × 6 runs ≈ 17 s, and only on PRs whose changed modules select them (Benchmarks: run only the benchmarks of the modules a change touches #133's USES). The full cross-product (every group, size and backend) goes to step 5.
  3. Report throughput. Read throughput from criterion's benchmark.json and add MiB/s columns for Base and Head. That costs no benchmark time.
  4. Say which backend ran. Add a line to the summary listing the CPU features the runner detected (cpu::detected), so a reviewer can see why a number is what it is. This is what would have caught GHASH on x86-64: carry-less products with mul (BearSSL ctmul64) #142 at a glance.
  5. Put the slow sweep on a schedule, not on PRs. A weekly (or workflow_dispatch) run of every benchmark on every backend, at every size plus one large size (e.g. 1 MiB) and a keyed-once variant that moves setup out of the loop. It compares with the previous week's commit through the existing base_commit input. This is where steady-state throughput and overall trends live, and it adds nothing to per-PR time.
  6. Optional: like-for-like OpenSSL rows. For scalar rows, run OpenSSL's reference with OPENSSL_ia32cap masking AES-NI and PCLMULQDQ, so "Head vs OpenSSL" compares against OpenSSL's own portable code rather than its AES-NI code. That's one extra OpenSSL run per affected group, and it can be dropped if it isn't worth the time.

Backends that need features no hosted runner has (e.g. future AVX-512/VAES) can't be timed meaningfully under SDE. Those rows should show "not benchmarked (runner lacks X)" instead of disappearing.

Order: 1 to 4 in one PR (bench crate and ci/bench_compare.py only, no library or Lean changes). 5 follows in its own PR, and 6 only if wanted. Once the first PR lands, #142's Benchmarks run should show the scalar speedup.

🤖 Generated with Claude Code

Activity

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

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions