Benchmark the exact primitive, operation, input size, feature set, and target that you plan to deploy. A crate-wide aggregate is not a deployment result.
benchmark_results/OVERVIEW.md owns the published summary.
Keep raw results and run metadata for every published claim.
A local measurement without that evidence applies only to the machine that made it.
Each new local or development-machine run writes its own directory:
benchmark_results/criterion/<run-id>/
Each run records:
- the literal requests and the log;
- source-file hashes;
- the compiler and Cargo identity;
- the build and runtime environment;
- CPU and operating-system information;
- the raw
criterion/data; status.txt.
Successful discovery adds the resolved case plan.
status.txt starts as running.
It becomes complete or failed, with the exit code of the execution.
complete needs, for every planned case, the case identity, the statistical samples, and the estimates.
It also needs the comparison estimates when a baseline was given for that case.
If the process succeeds but results are missing, the run fails.
A run that is interrupted abruptly can stay running.
If packaging fails, the run is kept and no checksum is published.
Source hashes identify a dirty worktree.
They do not replace keeping the source.
Export a run only when you need a portable artifact:
just bench-export benchmark_results/criterion/<run-id>This archives the run, including failed-run evidence, under benchmark_results/.transfers/ with a SHA-256 checksum.
Export refuses to overwrite an existing archive.
output_dir=<path> changes the results root, and keeps the criterion/ and .transfers/ layout.
On development machines, use the default root. Collection exports the selected run, then downloads it:
just ssh-collect-bench <target> <run-id> <new-local-directory>Collect before you destroy the machine. The destination must not exist yet. Historical result directories, named by date, operating system, and architecture, do not change.
Each run starts with new Criterion output. To compare with a completed run, select it explicitly:
just bench sha256 baseline=benchmark_results/criterion/<previous-run-id>- Only validated Criterion
basedata for matching cases and configurations is copied into the new run. The previous run does not change. - Compatibility includes the Cargo command, compiler, manifest profiles, Cargo configuration file hashes, CPU identity, build and runtime controls, and effective Criterion settings.
- The source revision and the watchdog budget are recorded, but they are not part of compatibility.
- A baseline needs a resolved plan and verified measurements.
- If no configuration or case matches, the command fails. Otherwise, cases without a match run without a comparison.
- Matching metadata does not prove the same thermal, power, or system-load conditions. Control those before you interpret a comparison.
speedup = comparison_time / rscrypto_time
A value above 1.00x favors rscrypto.
A value below 1.00x favors the comparison.
Summary tables treat 0.95x through 1.05x as a tie.
.config/benchmark-matrix.json owns the benchmark targets, required features,
selector names, filters, and comparison declarations.
The benchmark source owns each timed operation.
Inspect both before you claim equivalent work.
A comparison measures one task, such as "verify a signature with a key parsed once". It does not compare code paths. Every comparison follows these rules:
- A group measures one task.
When an operation has more than one task, the group name says which one,
for example
ed25519/verify/reuse-keyanded25519/verify/import-key. - Each library joins a group with its best public API for that task, and with its best supported build. A library that has no API for the task stays out of the group, and the source comment at the group says so.
rscryptoruns in its default build. In each group and input, exactly one row is namedrscrypto: its best public API for the task. Rows namedrscrypto-<variant>show otherrscryptoAPIs. They never enter a ratio.- Differences that come from a library's design, such as what it caches or how it selects instructions, are results. Differences that come from the harness, such as a weaker API, missing build flags, or skipped validation, are defects.
- Every row checks its output against an independent oracle before it is timed; see Output checks. Where a task includes validation, the check also requires the timed code path to reject a malformed input, when that is practical.
The source comment at each group states the task, what is timed, what each library caches or validates, and any remaining API-cost difference.
The catalog's comparisons map declares each comparison:
its task, the target that measures it, a case pattern, a family, and whether it is in the headline.
The headline holds each primitive's primary operations.
Streaming, batch, collection-hasher, construction, and parsing comparisons are reported by family, outside the headline.
just bench-compare x86_64-linux-intel=benchmark_results/criterion/<run-id> --json scorecard.json
just bench all --list > cases.txt
just bench-compare --cases cases.txt- Each argument is
PLATFORM=RUN_DIRECTORY. Repeat a label to combine runs from one host. The scorer rejects runs from different hosts under one label. - One comparison point is one comparison, variant, and input on one platform.
Its ratio divides the fastest external median by the
rscryptomedian. The JSON output also gives an interval from the two medians' confidence intervals. - A comparison can name
competitor_benches, which are other builds whose external rows also count. Therscryptorow always comes from the comparison's own target. - A comparison can restrict
architectureswhen no external library offers the operation elsewhere. Its requiredavailabilityexplanation names that limit. The rscrypto case still runs on every supported target. On an excluded architecture, the report retains it as a standalone measurement with the explanation, without a ratio. An unexpected external row there fails the report so the catalog must be updated when support changes. - The scorer fails when a point lacks its
rscryptorow or an external row, when a platform lacks a competitor build that applies to it, and when a measured group has external rows but no declaration.--partialreports the first kind as notes, for runs whose case filter omitted rows. --caseschecks the declarations against a case listing without measuring. It fails on a listing without case lines. It checks only comparisons whose binaries appear in the listing, and reports how many that is. It uses this host's architecture; add--architecture s390x, for example, for a listing from another host.
The Bench workflow's Score comparisons job extracts and scores the run's own measurement archives.
It writes the tables to the job summary and keeps the JSON as the bench-score-RUN-ATTEMPT artifact.
Published summaries come from that output.
If only the comparison report failed after every native measurement job passed, dispatch Bench with
rescore=RUN_ID/ATTEMPT, for example rescore=39106333869/1.
This mode ignores the measurement inputs and starts only a report job.
It requires a completed Bench attempt from this repository's main, successful native jobs,
and exactly one retained archive for every such job. Only the source report may have failed.
It revalidates every retained measurement and requires complete comparison groups.
The summary identifies the measured revision. The JSON also records the revision of the corrected report.
Re-scoring an older run does not measure the current source revision.
Some libraries choose their SIMD code only at compile time.
xxhash-rust compiles its SSE2, AVX2, or AVX-512 code only when the build enables those target features.
Default benchmark builds pass no CPU flags, so on x86-64 it would run SSE2,
while rscrypto selects AVX2 or AVX-512 at run time.
- The
xxh3_v4target builds the same binary with-C target-cpu=x86-64-v4, on x86-64 only. - The
xxh3comparison takes each competitor's fastest row from either build.rscryptostays in its default build. xxh3-baseline-buildkeeps the default build only. It shows what runtime dispatch gives a binary built for the x86-64 baseline.xxh3-v4-buildcompares every library compiled for x86-64-v4.- A binary compiled for CPU features that the host lacks refuses to start.
The catalog's architectures field limits a target to some architectures,
and rustflags builds a target as its own configuration with extra flags.
Profiling and code inspection do not apply catalog rustflags, so they reject those targets.
The Bench workflow runs only on manual request. Select the revision with the GitHub branch selector, then fill in the form:
| Input | Example | Meaning |
|---|---|---|
benchmarks |
sha256 |
One catalog algorithm. |
benchmarks |
sha256,blake3-mixed |
More than one name, separated by commas or spaces. |
benchmarks |
sha2, mldsa, auth |
A whole benchmark target. |
benchmarks |
hashes, checksums, mlkem |
A catalog group. |
benchmarks |
all |
Every default target. |
filter |
^sha256/rscrypto/64$ |
Limit each selected target to matching Criterion cases. |
| Platforms | x86_64-linux, riscv64-linux |
Check one box for each platform. |
quick |
Checked | Run every selected case with Criterion's quick sampling and report coverage. |
The platform checkboxes are x86_64-linux, aarch64-linux, x86_64-win, s390x-linux,
powerpc64le-linux, and riscv64-linux. Only x86_64-linux is checked by default.
Each x86-64 platform runs separate Intel and AMD jobs.
benchmarks accepts the same names as just bench; see Select cases.
The planner rejects unknown names, a form with no platform checked,
and targets that need an external prerequisite, such as tls-provider,
before it starts any build or measurement runner.
A case filter that matches nothing in a target fails that target's job during discovery.
Optional sampling fields override the shared Criterion settings in
.config/criterion.json. Empty fields keep those settings.
The diagnostic checkbox enables diagnostic features for the selected targets.
It does not select other targets.
Use quick for a short validation run. It keeps the selected cases, production operations,
independent output checks, and ten-minute invocation limit. Leave sample_size and warmup_ms
blank in this mode; measure_ms controls Criterion's quick measurement window.
The local equivalent for one algorithm is just bench hmac-sha256 quick=true measure_ms=10.
Quick artifacts record their sampling mode, and the report gives case and comparison coverage
without performance rankings. Quick runs cannot be used as performance baselines or mixed with
regular runs in one report.
A small planning job validates the request and groups the selected targets by primitive family.
The catalog gives each Criterion target one family:
checksum, hashes, aead, or auth, after the root modules, tls for the provider comparison,
or openssl for the OpenSSL primitive comparison.
Each checked platform and selected family pair becomes one job.
The job runs one just bench for each selected target of its family, one after another.
A split target, such as slhdsa, runs one just bench for each of its catalog algorithms instead;
see Time budget.
For example, sha256,blake3,crc32c on x86_64-linux runs four jobs.
Measure x86_64-linux-intel / hashes and Measure x86_64-linux-amd / hashes each run sha2, then blake3.
The two checksum jobs run crc.
The plan log prints the just bench command that each target runs.
Every platform receives the same plan.
A job skips a target that the catalog limits to other architectures, such as xxh3_v4 outside x86-64,
and prints the skip.
After every measurement job passes, the Score comparisons job scores the run; see Scorecard.
- AWS supplies fixed On-Demand instance types for Linux x86-64, Linux ARM64, and Windows x86-64. Both x86-64 operating systems run separate Intel and AMD jobs, with different artifact names.
runs-on.ymldefines the machine shapes. Benchmark preparation usesbench-cross-build. Measurement uses themeasure-*profiles. They are sized separately from the CI and CT profiles.- These profiles use the fixed processor families named in the catalog, with no AVX2-only baseline.
- IBM and RISE supply their existing native runners.
- macOS benchmarks run locally on the Apple Silicon Mac.
All preparation jobs start together, then all measurement jobs.
RunsOn starts an AWS host for every queued job and ignores max-parallel,
so the number of family jobs bounds the concurrent hosts.
Each RunsOn job has its own runner label, because a job can take any host whose label matches.
scripts/test/cross_workflow_test.py checks the largest possible run
against the account's Spot and On-Demand vCPU quotas.
- RISC-V, POWER, and IBM Z compile the selected targets of each family on x86-64
with
--ci-cross-build TARGET, in aPrepare PLATFORM / FAMILYjob. Their native jobs use--ci-cross-runwith the verified tools archive, then discover and measure the transferred binaries. - Other Linux jobs use
--ci-bench. Windows uses-CiBench.
No caches and no speed-regression gates are enabled. Donated hosts can be shared, and fixed AWS instance types do not remove host noise. Equal vCPU counts do not mean equal physical core counts. Use the recorded CPU topology to interpret parallel results.
Each measurement job keeps target/bench/ as one bench-RUNNER-FAMILY-RUN-ATTEMPT.tar.gz artifact,
for example bench-x86_64-linux-intel-hashes-<run>-1.tar.gz, with one run directory for each just bench.
GitHub stores the archive as uploaded, without zipping it;
zipped uploads of these directories crashed upload-artifact on the RISC-V runners.
The packing step runs after any job outcome, so failed and cancelled jobs keep their evidence.
It includes failed-run evidence, source and machine identity, the resolved case plan, logs,
and raw Criterion results.
A cross-target run omits its transferred binaries; input/bundle.json keeps their digests,
and the preparation artifact keeps the files.
Preparation jobs keep their evidence as bench-prepare-PLATFORM-FAMILY-RUN-ATTEMPT.
Each target, or each algorithm of a split target, runs as its own just bench,
so its time budget applies separately.
A target can still exceed its budget; a filtered or quick run checks only its stated scope.
A job stops at its first failed just bench.
Preparation jobs allow 30 minutes for one invocation,
plus 10 minutes for each additional invocation.
Measurement jobs allow 120 minutes, or 180 on RISC-V, for one ordinary invocation,
provisioning and transfer.
The planner adds 10 minutes for each additional invocation and 20 minutes
for each extended comparison.
One failed job cancels the rest of the run.
Manual dispatch is available after the workflow reaches the default branch.
Cross-build preparation never runs target code. It seals each unique catalog build configuration, the source identity, the compiler and linker evidence, the exact binary hash, and the requested sampling settings.
Native consumption rejects changed sources, settings, configurations, or ELF architectures before discovery. It uses the same case filtering, measurement, and result verification as an ordinary run, without compiling again. The retained input manifest identifies the build host. Result compatibility records the measurement host and its runtime settings. Cross-built and native-built results have different build identities for baseline comparisons.
The two invocations use matching selections and settings:
just bench ... target=TARGET prepare_archive=ARCHIVE
just bench ... target=TARGET run_archive=ARCHIVEThe same selections work locally:
just bench sha256 blake3
just bench hashes
just bench all
just bench sha2 'filter=^sha256/rscrypto/64$'The Profile workflow runs only on manual request. It takes one native Linux architecture and one curated primitive.
- The benchmark catalog maps each primitive to one benchmark target and one exact production case. It also owns the diagnostic-feature policy.
- The default selection is
aead/aes. It profilesaes-128-gcm/copy-and-encrypt/rscrypto/4096for five seconds. - The ML-DSA presets use ML-DSA-65:
auth/mldsa65-keygen,auth/mldsa65-prepared-sign, andauth/mldsa65-prepared-verify. Signing is deterministic. Signing and verification do not include key preparation. - The SLH-DSA presets are
auth/slhdsa-sha2-128s-sign,auth/slhdsa-shake-128s-sign, andauth/slhdsa-sha2-128s-verify.
The GitHub UI shows the same two inputs as the CLI:
gh workflow run profile.yml --ref BRANCH \
-f architecture=riscv64-linux -f primitive=hashes/sha256How a capture runs:
- The planning job validates the request before it reserves native hardware.
- An x86-64 job cross-builds and seals the production benchmark.
- The native job verifies the archive and discovers the case exactly once.
- It records with
perf record. - It writes a symbol hotspot report with
perf report --stdio --no-inline. The report includes call paths when the runner supports them. - The GitHub job summary shows the report.
The artifact keeps the exact binary, the raw
perf.data, the report, and the machine identity.
Collector setup:
- Native setup prefers the runner's
perf. - The pinned RISC-V kernel has no matching Ubuntu tools package,
so setup builds
perffrom the matching pinned upstream stable source. - Other donated runners fall back to the pinned Ubuntu generic userspace tool.
- The selected collector version is recorded next to the kernel identity.
- A live capability probe decides whether the tool and the host can collect evidence.
- When runner policy denies access, native setup enables perf events. The benchmark stays unprivileged.
The workflow keeps the evidence for missing packages, denied permissions, unsupported events, and partial captures. Native reports and raw evidence stay downloadable for 30 days, also when capture fails. Preparation and native capture each have a 20-minute cap. Planning has a five-minute cap. A newer request for the same architecture cancels an older request that is still running. Inspect the uncertainty, and repeat matched measurements, before you make a performance claim.
Every Criterion row checks its output before Criterion times it.
- Rows register through
checked,checked_with, orchecked_routineinbenches/common/criterion.rs. - The check runs once per row, outside timing, when Criterion first runs the row. Listing runs no checks.
- The check receives the operation's second output, so reused state has already served one call.
- Every row, comparison rows included, must match an independent oracle: another implementation or a published vector. An independent verifier checks randomized outputs, such as hedged signatures and new keys.
- Two row families have no independent implementation to compare with. Ascon-CXOF128 rows reproduce a published vector and match rscrypto's incremental API. RapidHash collection hashers, which no external specification defines, match pinned known answers.
- A mismatch stops the run before that row is timed.
- A planned run fails when a measured row skipped its check.
Each binary prints
Output checks: N of N measured rows passed, and the Bench workflow adds one such line per binary to its job summary.
The optional blake3_file target measures Blake3::update_file, Blake3::update_reader,
four caller-scheduled subtrees, upstream update_reader and update_mmap_rayon, and standalone b3sum.
Discover its cases
with just bench blake3_file --list. It is excluded from default runs because
it needs explicit file fixtures and Linux cache-residency checks.
The artifact printed by discovery accepts --rscrypto-prepare-file DIRECTORY BYTES
to create one new deterministic file and digest manifest, and
--rscrypto-verify-file DIRECTORY BYTES to check the library operations
and the CLI when configured. The fixed
sizes include short files and stack/heap boundaries before the 1 MiB, 16 MiB,
256 MiB, 1 GiB and 10 GiB bulk cases. The benchmark source defines the exact sizes. Preparation refuses to overwrite
a fixture. Set RSCRYPTO_BLAKE3_FILE_DIR to that directory and
RSCRYPTO_BLAKE3_FILE_CACHE_LOG to the retained JSONL output path before invoking
just bench blake3_file with the desired case filter.
The upstream-b3sum-4 rows require RSCRYPTO_BLAKE3_B3SUM and
RSCRYPTO_BLAKE3_B3SUM_RECEIPT: an executable and a retained build receipt for
the same upstream revision as the locked BLAKE3 dependency. benches/blake3_file.rs defines the required receipt fields. --rscrypto-verify-b3sum validates the receipt,
binary hash and version without hashing a file. A receipt attests its build;
retain the source and build logs that substantiate it. The shared benchmark
metadata hashes both external files, preventing baseline reuse after a change
at the same path. Discovery and filtered library rows require neither file.
Cold cases condition the file and check residency with fincore before every
operation, outside timing. Cold cases require zero resident pages after file-scoped
eviction advice. By default, operations are unpaced and can reach the storage service's
throughput limit. RSCRYPTO_BLAKE3_FILE_COLD_MIB_PER_SEC adds a delay of file size divided
by the selected integer MiB/s before each cold conditioner, outside timing. Zero or an
unset value keeps the unpaced method. A positive value measures cold-operation latency
at that explicit arrival rate. The benchmark metadata records the value; keep paced
and unpaced results separate and retain the storage throughput configuration.
Warm conditioning checks residency before the initial output check and reads the file once only if pages are missing, then checks again. The output check also reads the file. Each Criterion batch then verifies residency before and after timing without rereading or rewarming the fixture. Every file page must remain resident, including a partial final page. This does not promise CPU-cache warmth, and results must not be pooled with the older method that reread every warm file or checked warm residency before every operation. A failed check stops measurement and retains the observation. These are Linux page-cache states, not claims about device or storage-service caches. A host must have enough available memory for the warm fixture; a shared machine is unsuitable for cache qualification. Retain the fixtures' manifests, cache log, host/storage identity, activity observations and complete Criterion output with the result.
Timing includes open/seek, file metadata and position queries in update_file, hash-state construction, reading, allocation and input
cleanup, finalization, and destruction. The subtree row also includes scoped
thread creation/join and public parent merges. A shared four-worker Rayon pool
is initialized before timing for upstream and the Linux AArch64 plain reader.
Pool startup is therefore excluded; subtree thread creation stays included.
The standalone CLI includes command construction, process and four-worker
startup, hashing, output capture and exit in every timed call. Its provenance
and correctness checks run outside timing.
The comparison reports those actual
caller costs. It enables mmap only in the upstream development dependency;
rscrypto's production reader uses ordinary reads. The benchmark source owns
these boundaries. This target does not qualify short-prefix streaming or keyed
file performance.
rscrypto BLAKE3 can use Rayon threads only with the parallel feature, which is not a default feature.
Two catalog targets build the same binary:
blake3_serial, withoutparallel: single-thread groups underblake3/serial/:hash,keyed,derive-key/context,derive-key/prepared-context,xof,xof-output,bao-decode,update-reader,subtree-update-reader,ordered, andordered-alignment.xof-outputsqueezes 1 KiB, 16 KiB, and 1 MiB from a 32-byte seed into a reused buffer.bao-decodeverifies a whole Bao combined encoding of 1,024, 1,025, 65,536, or 1,048,576 bytes from memory against bao 0.13.1, the format's reference.baocalls its one-shotdecode, which returns a newVec;bao-readerandrscryptoread their decoders to EOF into a reused buffer. Each check also requires the timed path to reject the encoding with its last byte changed.update-readerhashes 4 KiB, 64 KiB, 1 MiB, and 8 MiB from a&[u8]reader:Blake3::update_reader, which coalesces reads into an aligned 1 MiB heap buffer cleared before deallocation, against upstreamHasher::update_reader. Construction, every read and update, the buffer, and finalization are timed.subtree-update-readerhashes a 64 KiB or 1 MiB subtree, starting at the offset equal to its length, into its non-root chaining value:Blake3Tree::subtreeandBlake3Subtree::update_readeragainst upstream's hazmatset_input_offset,update_reader, andfinalize_non_root.orderedincludes 3, 5, 6, 7, 11, and 15 complete chunks before a 1-, 70-, or 1,023-byte tail: the one-pass NEON partial-chunk kernel takes 3, 7, 11, and 15, the AVX-512 one takes 3 and 15, and 5 and 6 take neither. Upstream rows use its one-shot orHasherAPIs, which are single-threaded. The headline uses thehashandkeyedgroups, because a default install hashes on one thread.blake3, withparallel: groups that allow threads, underblake3/parallel/:hash,keyed,derive-key/context,xof, andupdate-reader.rscryptothreads when its CPU profile's cost model admits the input. Inupdate-reader,rscryptohashes complete 1 MiB buffers in the Rayon pool on little-endian Linux AArch64; upstream has no threaded reader API, so its row is serial.blake3is upstream's single-threaded API, andblake3-rayonisHasher::update_rayon. At each size, the faster upstream row counts. Both libraries use the global Rayon pool, which starts before timing. Report these results with the host's core and thread topology.- The
blake3build also runsbatch,mixed-batch,streaming, andmerge-level. No row of those groups uses threads.merge-levelmerges 1, 16, or 256 pairs of chunk chaining values:Blake3Tree::merge_level, which validates every pair and hashes up to 16 parents per kernel call, against one hazmatmerge_subtrees_non_rootcall per pair, which validates nothing. Its check also requiresmerge_levelto reject swapped children. derive-key/contexthashes the context string in every timed call, on both sides.derive-key/prepared-contexthashes it before timing. There,rscryptopasses aBlake3DeriveContext;rscrypto-cachedcallsBlake3::derive_key, which reuses the context from a per-thread cache; and upstream builds a hazmatHasherfrom the context key in each call.
just bench blake3-mixed compares batch and serial rscrypto calls with locked
upstream serial calls over the same ordered inputs. Its 54 rows span plain,
keyed and derive-key modes, three length mixes, and 16 or 65 inputs. Fixture
allocation, correctness checks and derive-context hashing stay outside timing;
production construction, internal cleanup and output writes remain timed.
Each library retains its own cleanup policy. Caller output storage is reused.
RSCRYPTO_BLAKE3_BENCH_ISA selects auto, portable, sse41, avx2, avx512
or neon before cached detection. The override only removes capabilities and
rejects unavailable tiers. Upstream remains explicitly labeled blake3-auto.
Process output records capability masks; optional --diag also checks the
selected bulk backend. Native performance qualification is separate from
compilation, case discovery and output equality.
Choose the timed boundary from the question that the workload answers. Write it next to the benchmark group in the source. Include input restoration, allocation, key or state construction, output handling, and destruction. Put material included or excluded work in the case identity. Implementation names must identify the library or backend that is actually called. A renamed boundary starts a new baseline.
- Reusable-buffer operation: allocate storage and prepare reusable state outside timing. Time the operation on that state. If the operation needs fresh state, describe any batched setup explicitly. Do not call its allocation part of the measured operation.
- Copy plus operation: inside timing, restore the input into preallocated storage,
then run the operation on it.
Use
copy-and-…in the operation name. Use this boundary to measure the cost of keeping an immutable source message. - Complete application operation: include the real lifecycle under study, and name its stages,
for example
copy-and-construct-and-seal. State which application costs are still excluded. Constructing a cipher does not mean that packet allocation, entropy, or transport is included.
Do not move setup out of timing only to get a smaller number.
itertimes the work and destruction inside its closure, and the destruction of its return value.iter_batchedexcludes the setup closure and defers destruction of the returned output. Consumed inputs can still be destroyed inside the timed closure.iter_batched_refalso defers destruction of the setup object.
The P-256 ECDH batches prepare new consumed keys outside timing. RapidHash map-insertion batches allocate empty maps outside timing. These boundaries are different from the timed buffer restoration in AEAD rows.
The AEAD copy-and-encrypt, copy-and-decrypt, copy-and-seal, and copy-and-open groups reuse preallocated buffers and cipher contexts.
- Timed: input restoration, cryptography, and per-call output handling and cleanup.
- Not timed: fixture generation, the first buffer allocation, and construction and destruction of the reusable context.
- Rows labeled
appended-tagcopy or produce the combined ciphertext-and-tag form. Other rows use detached tags. - Throughput counts message bytes, not restoration traffic or tag bytes.
- AES-SIV
copy-and-construct-and-sealalso constructs and destroys a cipher in each iteration. copy-and-construct-and-encryptmeasures one message per key for the other AEADs, at 64 bytes and 1 KiB: each call builds a cipher from the key bytes, restores the input, encrypts with a detached tag, and destroys the cipher. ring and aws-lc-rs build anUnboundKeyand aLessSafeKey; the aegis crate binds the nonce when it builds its cipher. Decryption builds the same key schedule and has no separate group.
These are not cryptography-only measurements, and they are not complete packet-processing measurements.
The groups compare the RFC 9001 header-protection APIs of rscrypto, ring, and aws-lc-rs.
RustCrypto has no such API, so its rustcrypto-composed rows build the mask from aes and chacha20.
construct: build a reusable context from borrowed key bytes, then destroy it, in each timed call.rscryptoalso copies the bytes into its typed key, and zeroizes that key and the context. ring does not zeroize. aws-lc-rs builds an unused AES decrypt schedule and zeroizes both AES schedules. RustCryptoaesis built without itszeroizefeature. RustCrypto has no ChaCha20 construct row, because it has no key-only ChaCha20 object.mask: compute one five-byte mask with a context built outside timing. ring and aws-lc-rs take a slice and return aResult. The composed AES row encrypts a full block. The composed ChaCha20 row keys a new core on every call.- Every row must reproduce the RFC 9001 Appendix A.2 and A.5 masks before timing, plus the bench-sample mask on which RustCrypto and ring agree. AES-256 has no published vector, so that agreement is its only oracle.
The suite retains multiple implementations and scores the fastest available external row for each exact workload. An added comparator is a candidate for measurement, not a claim that it wins on every CPU.
- BLAKE2 includes
blake2b_simdandblake2s_simdwith their default runtime CPU detection. The rows call serial BLAKE2 with matching digest lengths, keys, salt and personalization; BLAKE2bp/sp and parallelmanyAPIs compute different workloads. In the selected implementation, serial BLAKE2b can use AVX2, while serial BLAKE2s uses SSE4.1. BLAKE2s's AVX2 path processes multiple independent inputs. - XXH3 includes
twox-hashwithstdruntime dispatch: AVX2 or SSE2 on x86, NEON on AArch64, and a portable fallback. It joins one-shot, collection-hasher and lookup rows with the same default seed and input bytes. Collection rows include both its allocatingHasherand its allocation-freeRawHasher, which borrows the default secret throughSecretBuffer::default(). Both time hasher construction, every update and finalization. Keep thexxhash-rustcomparison and the explicit x86-64-v4 build; runtime dispatch and a build that assumes AVX-512 are distinct deployment choices. - Adler-32 compares with
simd-adler32andzlib-rs, which select a SIMD kernel for the CPU, such as AVX2 or NEON. rscrypto's Adler-32 also has per-architecture kernels and a portable fallback. Platforms lists the kernels.zlib-rsis the only comparator with a combination, so it alone supplies theadler32/combinerows. Its AVX-512 kernels need itsavx512feature and an AVX-512 build, which the default Bench build is not. - XXH32 and XXH64 compare with the portable Rust
xxhash-rustandtwox-hashcrates, with seed zero. Thechunk-64streaming rows write 64-byte slices and include construction and the digest. - AWS-LC's POWER64LE GNU dependency was restored for ML-DSA in
ecde7e65. The other benchmarks retained older gates and silently omitted their AWS-LC rows. All AWS-LC benchmark gates now share one target definition, matching the dev dependencies inCargo.toml. The POWER-only dependency enables the pinned AWS-LC's bindgen support and needs libclang at build time. Cross-builds select AWS-LC'sccbuilder: the pinned CMake helper loses the target's little-endian identity and omits its POWER assembly, causing link failures. This enables the same production operations on POWER64LE; native POWER measurements still determine their speed. - The optional
openssl_primitivestarget compares SHA-2, SHA-3 and AES-GCM through OpenSSL EVP. OpenSSL exposes IBM Z capability detection for CPACF. Use a native IBM Z run with assembly enabled, the loaded library identity and capability overrides retained, before drawing a conclusion about CPACF performance. The primitive guide defines its timing and setup. Include these optional comparison results in SHA and AES assessments; the default suite's scores do not include OpenSSL.
The shipped provider rejects foreign C dependencies in its normal/build graph;
that gate does not reject benchmark dev dependencies or independent comparison workspaces.
Ordinary cross-target library linting avoids dev dependencies by selecting --lib.
Benchmark transfer instead supplies target C compilers.
An external OpenSSL comparison additionally needs target headers and libraries;
the default Bench workflow does not provision them and excludes this optional target.
- Byte-oriented one-shot groups share one size matrix: 0 and 1 byte, 32 bytes, and powers of two from 64 bytes to 1 MiB.
Hash groups add the lengths at which their primitive changes regime, in the same group:
- SHA-224 and SHA-256: 55 and 56 bytes, the longest message whose padding fits one block and the shortest that does not. SHA-384, SHA-512, and SHA-512/256: 111 and 112.
- SHA-3 and SHAKE: one byte below the rate, and the rate (143/144, 135/136, 103/104, 71/72, 167/168).
- XXH3-64 and XXH3-128: 16/17, 128/129, and 240/241, where the short-input algorithm changes.
- XXH32: 15/16. XXH64: 31/32. The longer input fills one stripe.
- Adler-32: 15/16, where the portable 16-byte step starts, and 128 and 512, either side of the x86-64 kernels' 256-byte switch to their chunked loops.
- BLAKE2: 65 and 129, one byte past each block size. BLAKE3
hashandkeyed: 65 and 1,025, one byte past a block and a chunk.
- Ed25519 and ECDSA verification has two tasks.
verify/reuse-keybuilds each library's key object outside timing and times verification.rscryptoand Dalek reuse the decoded Ed25519 point.rscryptoreuses its ECDSA verification table, and RustCrypto reuses aVerifyingKey. aws-lc-rs reuses aParsedPublicKey, which saves parsing and allocation. AWS-LC still decodes the Ed25519 point in each call, and re-encodes an ECDSA signature as DER in each call. ring and dryoc have no API that reuses a parsed key, so they are absent.verify/import-keygives every library the same encoded key and signature bytes, and times key import and validation, signature parsing, verification, and destruction, as a TLS certificate or handshake check does. ECDSArscryptoisverify_sec1, which skips the verification table that key reuse amortizes, as rscrypto-rustls does for a key that verifies once. aws-lc-rs allocates anEVP_PKEY.- Each check also requires the timed path to reject a key that is not a curve point.
- The diagnostic
verify-phase/portable-double-scalarandverify-phase/aarch64-asm-double-scalarrows include portable public-point decoding and result encoding; they do not measure cached-key verification alone.
- ECDSA
sign/rscryptois the defaulttry_signAPI, which needs no entropy source.rscrypto-blindedreturns the same signature through the blinded path. - ECDH agreement has two tasks.
agreement/parsed-peerparses and validates the peer key outside timing, and times the scalar multiplication. aws-lc-rs clones itsParsedPublicKeyin each call, becauseagreeconsumes the peer. ring and libcrux have no parsed-peer API, so they are absent.agreement/encoded-peergives every library the NIST peer key as identical uncompressed SEC1 bytes, and also times parsing and validation. X25519diffie-hellman/encoded-peerbuilds the peer key from its 32 bytes inside timing in every row. Dalek also runs its contributory check. Each check also requires the timed path to reject an invalid peer. - ECDH
key-generationturns a caller scalar into a private key, without deriving the public key.public-keyderives the public key from a private key that the caller holds.public-key-from-secretimports the scalar bytes and derives the encoded public key. aws-lc-rs takes part only inpublic-key-from-secret, because its key import already derives and caches the public point. libcrux has no key object, so it is absent frompublic-key. ring cannot import a scalar, so it is absent frompublic-key-from-secret. - The ChaCha diagnostic group
chacha20-copy-and-xorrestores the message and applies the keystream in the timed closure, with a reused allocated buffer. It is not a complete AEAD operation. - CRC
streamingchecksums 1 MiB through updates of 64 bytes or 4 KiB, with each library's incremental API. CRCcombinejoins the checksums of a 1 KiB part and a second part of 64, 65,535, or 16,777,215 bytes. Those lengths have every low bit set, which gives square-and-multiply combination its most multiplications. crc32fast combines two hasher states built from each checksum and length; those constructions stay timed. CRC-24/OpenPGP has no comparison library with combination, so its group has only therscryptorow. - SHA-3 and SHAKE rows compare RustCrypto (
sha3, andshakefor SHAKE), AWS-LC, and tiny-keccak. RustCrypto's Keccak selects the AArch64 SHA3 instructions at run time. AWS-LC runs s2n-bignum Keccak assembly on x86-64 and AArch64, and portable C elsewhere.aws-lc-rsrows use its digest API for SHA3-256, SHA3-384, and SHA3-512.aws-lcrows call AWS-LC's one-callEVP_Digestthrough aws-lc-sys for SHA3-224 and SHAKE, which aws-lc-rs does not expose; it allocates and frees its hash state in each call. SHA3-224 streaming has no AWS-LC row.just bench openssl-sha3-224supplies an OpenSSL one-shot and streaming comparison through safe bindings, including 1 MiB streamed in 64-byte and 4 KiB chunks. It does not stand in for AWS-LC; adding AWS-LC streaming would require a new unsafe binding boundary. - SHAKE
xof-outputabsorbs a 32-byte seed and squeezes 1 KiB, 16 KiB, and 1 MiB into a reused buffer. The one-shot SHAKE groups squeeze 32 bytes. dryoc also joinsxof-output. - Poly1305
authenticatereads immutable fixture bytes, sets up a one-time key, and returns a tag, without restoring a message buffer. RustCryptopoly1305, dryoccrypto_onetimeauth, and AWS-LC's publicCRYPTO_poly1305_*API through aws-lc-sys are the comparison rows. ring and aws-lc-rs expose no standalone Poly1305. Cleanup policies differ:rscryptoclears its key and state, and AWS-LC leaves its caller's state uncleared. - BLAKE2
short-oneshotandshort-keyedkeep only the 16-byte and 128-byte inputs that the main size matrix does not have.single-updatemeasures construction, one update, and finalization at the small sizes. It is different from the multi-chunk streaming workload. Plain parameter-group duplicates are removed. The main one-shot rows are the baselines for salt and personalization hashing. All of these are complete hash operations, not isolated host overhead. - BLAKE3
orderedrows compare identical ordered bytes through one-shot, one-update, and two-update calls. Prefix/bulk and bulk/suffix cases have separate inputs; reversing their byte order is not an equivalent comparison. Construction, output, and destruction stay timed; fixture allocation does not. Derive rows hash the context string in every call, on both sides. These groups run inblake3_serial. Upstream'szeroizefeature is disabled, so cleanup costs are not equivalent. Every measured operation is checked against upstream before timing. - BLAKE3
ordered-alignmentrepeats selected ordered cases with identical bytes at addresses congruent to 0 and 1 modulo 64. It retains theorderedtiming boundary. Diagnosticleaf-alignment-cvsrows call existing full-chunk workers with independently controlled input and output alignment. They include wrapper validation and consume every CV, with allocation outside timing. Every CV is checked against upstream's non-root hash at its chunk offset. These component rows do not measure public dispatch or complete hashing. - HKDF
expandbuilds every pseudorandom key outside timing, from the same PRK bytes. aws-lc-rs usesPrk::new_less_safe, because itsSalt::extractonly stores the inputs and would run Extract again inside everyfill. AWS-LC'sHKDF_expandstill keys HMAC in each call: two more compressions than the libraries that keep a keyed state. - HKDF-SHA256 and HKDF-SHA384
expand-label/import-prkis the TLS 1.3 key-schedule step: import a hash-length PRK, then expand anHkdfLabelgiven as six slices, with the PRK import and its HMAC key schedule timed.derive-secretexpands "c hs traffic" with a hash-length context to HashLen bytes;traffic-keyexpands "key" with an empty context to 16 bytes.rscryptoisfrom_prkandexpand_vectored, RustCryptofrom_prkandexpand_multi_info, and ring and aws-lc-rsPrk::new_less_safeandexpandwith the parts. - PBKDF2 rows derive into a reused output buffer and time password keying, every iteration, and the output.
Below the password policy (1, 100, and 1,000 iterations),
rscryptocallsderive_key_primitive, because the policy API rejects those counts. At the policy minimum (600,000 iterations for SHA-256 and 220,000 for SHA-512) with one output block,rscryptocalls the policy APIderive_key, which also checks the iteration count and salt length. Theverifygroups at the same counts also compare with a stored key:rscryptocallsverify_password, which also bounds the work, and ring and aws-lc-rs callpbkdf2::verify, which check neither. RustCrypto'spbkdf2has no raw-key verification API, so it is absent there. Each verification check also requires the timed path to reject a stored key with one changed byte. - RSA
verify-*/reuse-keyprepares the key outside timing, and each call verifies one signature.rscryptoreuses a scratch that caches R²;rscrypto-oneshotallocates scratch in each call and enters the Montgomery domain by one division by the modulus instead. aws-lc-rs reuses aParsedPublicKey, which caches its Montgomery context, and builds anEVP_PKEY_CTXin each call. RustCrypto keeps only n and e, and converts the signature in each call. ring has no prepared-key API, so itsRsaPublicKeyComponentsrow rebuilds the modulus in each call. - RSA
verify-*/import-keyparses identical PKCS#1 bytes, validates the key, and verifies, in each call. All four libraries reject non-minimal DER and an even modulus.rscryptorequires e = 65537 and its policy's size range. ring, AWS-LC, and RustCrypto accept odd e from 3 to 2³³−1. RustCrypto stops at 4096 bits; aws-lc-rs checks its 2048–8192-bit range only when it verifies. - RSA
parse-spkiparses and validates identical SPKI bytes. aws-lc-rs tries PKCS#1 first and copies the input. ring has no SPKI parser. RustCrypto's parser stops at 4096 bits. At 8192 bits, AWS-LC is the only external parser in this suite. The root suite excludes AWS-LC on IBM Z, so that platform retains the rscrypto 8192-bit measurement without a comparison; the catalog requires the external row on x86-64, ARM64, RISC-V and POWER64LE. - RSA signing and OAEP name their entropy:
fixedis a caller blinding pair,caller-entropyis a fill closure or RNG that writes fixed bytes, andfresh-entropyis new randomness in every call.rscryptoand RustCrypto usegetrandom. aws-lc-rs uses AWS-LC's DRBG and a cached blinding pair. ring signs without blinding. RustCrypto and aws-lc-rs allocate their outputs inside timing. PKCS #1 v1.5 signing pads deterministically, so itsfresh-entropygroup draws only blinding randomness, and every row must produce RustCrypto's unblinded signature. The rscrypto-onlycached-blindingsigning groups reuse oneRsaBlindingCache: it draws a new blinding factor every 32 signatures and squares its pair in between, the schedule that aws-lc-rs uses in thefresh-entropygroups. PSS still draws a fresh salt in every call. Key generation stays outside the headline, because prime search makes its time vary widely. - RSA
pkcs1v15-encryption/decrypt/fresh-entropydecrypts a valid Wycheproof RSAES-PKCS1-v1_5 vector with fresh blinding. ring has no RSA decryption.rscryptoapplies implicit rejection: every call hashes the private exponent, keys HMAC-SHA256 with the ciphertext, and expands a modulus-length substitute message, which it returns only for invalid padding. RustCrypto 0.9 and AWS-LC return an error for invalid padding, so they skip that work. The difference is a design result, and the group stays outside the headline as a legacy operation. - HMAC-SHA2 verification has two tasks, at 32 and 1,024 bytes.
verify/reuse-keyprepares each key outside timing:rscryptoabsorbs into its keyed state, verifies, and resets it; RustCrypto clones its keyed state; aws-lc-rs and ring callhmac::verifywith a preparedhmac::Key.verify/import-keykeys from bytes in every call;rscryptocallsverify_tag. Each check also requires the timed path to reject the tag with its last bit flipped. - ECDSA
signature-der/encodeandsignature-der/decodetime the RFC 3279Ecdsa-Sig-Valuecodec alone, against RustCrypto. ring and aws-lc-rs parse DER only inside verification, so they are absent. Every encoding must equal RustCrypto's and verify under ring's DER verifier. Every decoding must return the fixed-width signature, and the timed path must reject the encoding without its last byte. - ECDSA
verify-der/reuse-keyincludes DER parsing, message hashing and verification with reused parsed keys in rscrypto, RustCrypto and AWS-LC. AWS-LC receives its native ASN.1 signature form, so it does not convert fixed-width signatures to DER.verify-der/import-keyalso imports the same uncompressed SEC1 key and includes ring's ASN.1 verifier. Itsrscryptorow isverify_sec1, the single-use path rscrypto-rustls takes;rscrypto-parse-keyisfrom_sec1_bytesfollowed byverify, which also builds the verification table. Ring has no parsed-key reuse API and is absent from the reuse group.verify-der-sha384(P-256) andverify-der-sha256(P-384) repeat both tasks with the curve's other TLS hash. Thererscryptoisverify_sha384/verify_sha256andverify_sec1_sha384/verify_sec1_sha256; RustCrypto hashes the message on the timed path and callsPrehashVerifier::verify_prehash, because its message API uses only the curve's own hash. Every row verifies an independently generated signature and rejects changed scalars and truncated DER. - HMAC-SHA3 rows time one tag from a key set up once outside timing.
rscryptoandaws-lc(AWS-LC's CHMAC_CTXthrough aws-lc-sys) both cache the Keccak states after the ipad and opad blocks.aws-lcreuses them throughHMAC_Init_exwith a null key, inside timing.rustcrypto-simplehmaccaches the ipad state and the raw opad block, and absorbs the opad block again for every tag. That is one more Keccak permutation per tag: 1.5x the work below the rate, and under 1.01x from 16 KiB. RustCrypto'shmac::Hmacwould cache the opad state, butsha30.12 does not expose the block-level core it needs. The aws-lc-rs and ring APIs have no HMAC-SHA3. Theaws-lcrow exists only where aws-lc-sys is a dev-dependency. - Ascon's
rscrypto/scalar-looprows compare repeated calls to therscryptoscalar API with its batch API. They do not compare with an external library. - RapidHash
rapidhash-v3-64andrapidhash-streamrows compute the same C++ RapidHash V3 function in both libraries. Therapidhash-buildhasher,rapidhash-hash-one, andrapidhash-hashmaprows compare three different collection hashers. rscrypto mixes buffered integer writes in before each byte write and ends with one mix. Upstreamrapidhash-fastkeeps integers buffered and has no avalanche round, so it does less work. Upstreamrapidhash-qualityadds avalanche rounds, so it does more. Quote an rscrypto collection result against both rows.
Compare only rows in the same operation group, with matching build and host identities.
These contracts replace the old ML-KEM IDs, and the Argon2 IDs without salt16-raw32.
Do not reuse those old measurements as baselines.
The effect of the old mismatches on the reported ratios has not been measured.
ML-KEM uses fixed 64-byte key-generation seeds,
and fixed 32-byte encapsulation randomness in the derand groups.
Fixture construction is not timed.
Decapsulation uses the same key material and ciphertext in every implementation.
It consumes no entropy.
Each parameter set has its own groups.
| Operation suffix | Timed input and preparation | Timed output |
|---|---|---|
keygen/derand-encoded |
Seed from the caller; generation and export | Encoded public key and expanded secret key as fixed byte arrays |
keygen/internal-entropy-encoded |
AWS-LC generation, internal entropy, and export | The same key encodings as fixed byte arrays |
keygen/derand-ephemeral-share |
Seed from the caller; generation, export of the encapsulation key, and decapsulation of one ciphertext built before timing for that key | Encoded public key and 32-byte shared-secret array |
encapsulate/derand-reuse-key |
Each library's best reused key, checked once outside timing; randomness from the caller | Ciphertext array and 32-byte shared-secret array |
encapsulate/derand-reuse-encoded |
Reused rscrypto, libcrux, or fips203 encoded-key wrapper; caller randomness; per-call decoding stays timed |
The same ciphertext and secret arrays |
encapsulate/derand-import-encoded |
Identical public-key bytes; each API's import, validation, and encapsulation with caller randomness | The same ciphertext and secret arrays |
encapsulate/internal-entropy-import-encoded |
The same public-key bytes; AWS-LC import and internal entropy | Ciphertext and secret converted to fixed arrays |
decapsulate/prepare-key |
rscrypto only: the preparation that its reuse-key row amortizes |
A prepared key |
decapsulate/reuse-key |
Each library's best reused key, and the ciphertext | 32-byte shared-secret array |
decapsulate/reuse-encoded |
Reused rscrypto, libcrux, or fips203 encoded-key wrapper and typed ciphertext; per-call decoding stays timed |
32-byte shared-secret array |
decapsulate/import-encoded |
Identical expanded secret-key and ciphertext bytes; each API's import, validation, and decapsulation | 32-byte shared-secret array |
- The
reuse-keygroups are the task view: the keys cache different work, by design.rscrypto's prepared key holds the decoded key, the expanded public matrix, and H(ek), plus the decryption mask for decapsulation.- RustCrypto's key holds the decoded key and H(ek), and samples the matrix in each call.
- libcrux and fips203 keys hold the encoded bytes, so decoding and matrix sampling stay timed.
- AWS-LC cannot take caller randomness, so it joins only decapsulation. There it holds the encoded key and runs the hash check in each call.
- The
reuse-encodedgroups compare equal key depth. There,rscryptoalso runs the FIPS 203 key check in every call, and libcrux and fips203 checked once outside timing. - Import rows run the FIPS 203 checks inside timing.
libcrux import does not check, so its rows call
validate_public_keyorvalidate_private_key.rscryptoruns the check twice: intry_from_slice, and again inKem::encapsulateorKem::decapsulate. Each import check also requires the timed path to reject a key with a coefficient of at least q, or a changed H(ek). - All ML-KEM rows include output conversion and the destruction of per-call objects.
Criterion::iterincludes the destruction of returned arrays. - Reused keys are constructed and destroyed outside timing. Imported keys are constructed and destroyed inside timing.
- Internal allocations and their cleanup stay timed, including the ciphertext and shared-secret buffers and key objects that AWS-LC allocates.
- The harness does not supply reusable scratch storage, and it does not equalize the cleanup policies of the libraries. The rows measure the selected APIs on valid inputs. They do not show identical validation or zeroization guarantees.
- RustCrypto takes part in encapsulation and in reused-key decapsulation. Its decoded secret key is built from the fixture seed outside timing.
keygen/derand-ephemeral-shareis a TLS client's key share: it generates a key, exports the encapsulation key, and decapsulates one ciphertext with the new key.rscryptoisgenerate_prepared_keypair_in, whose prepared key keeps the matrix that generation expanded;rscrypto-unpreparedisgenerate_keypairfollowed byKem::decapsulate. RustCrypto builds its key from the seed inside timing, and libcrux and fips203 decapsulate from the encoded key. AWS-LC generates keys only from internal entropy, so no ciphertext for its key exists before timing, and it is absent.- Expanded-key generation and import rows use implementations that have expanded-key APIs.
Correctness checks before timing:
- Each deterministic ML-KEM row runs its actual timed closure once outside timing, and checks the complete output against the shared fixture.
- Key-generation checks compare both encoded keys. Encapsulation checks compare the ciphertext and the secret. Decapsulation checks compare the secret.
- AWS-LC's randomized generation and encapsulation closures are checked through decapsulation by another implementation.
- A mismatch stops execution before that row is timed.
- Discovery lists identities only. It does not replace the correctness checks of the selected rows.
Argon2 comparison groups include salt16-raw32 in their IDs.
- All rows use the same password, the full 16-byte salt, Argon2 version 0x13, the same memory, time, and lane parameters, and 32-byte raw output.
- They consume no entropy and do no PHC encoding.
- Parameter objects and caller-owned output buffers are prepared outside timing.
- Each call includes the selected API's scratch allocation, computation, and scratch cleanup.
- The output buffer is reused, and destroyed outside timing.
- Each library keeps its own cleanup policy.
- Untimed checks compare all 32 output bytes of every row with RustCrypto.
- Argon2 parallel-scaling rows use the same salt and output size, and change the lane count.
- scrypt and PHC fixtures are separate workloads.
The reused-memory groups (argon2id-owasp/salt16-raw32-reused-memory and scrypt-owasp-reused-memory) isolate work-memory reuse at the OWASP shapes.
rscrypto/fresh-allocationcallsderive, so its allocation, zero fill, computation, and cleanup are timed.reused-memoryrows lend one buffer that is allocated and freed outside timing.rscryptostill clears every block it used, inside timing.rustcrypto/reused-memorycallshash_password_into_with_memory, built without RustCrypto'szeroizefeature. It does not clear its buffer, so the rows do not do equal cleanup.- Untimed checks compare all 32 output bytes with RustCrypto, after a call has already used the memory.
The PHC verification groups (argon2id-phc-verify and scrypt-phc-verify) verify one password against one stored PHC record.
- The record is created once outside timing. Record parsing and limit approval, derivation, the constant-time comparison, and work-memory handling are timed.
rscrypto/fresh-allocationcallsverify_password, which allocates, zero-fills, and clears its work memory in each call.rscrypto/reused-memorycallsverify_password_with_memorywith one buffer allocated and freed outside timing.rscryptostill clears every block it used, inside timing.rustcrypto/fresh-allocationcallsPasswordVerifier::verify_passwordon the same PHC string, parsed in each call. It allocates its work memory in each call and, built without RustCrypto'szeroizefeature, does not clear it.dryoc/fresh-allocation(Argon2id only) callscrypto_pwhash_str_verify, which parses the record and allocates its work memory in each call.- Only the fresh-allocation rows enter a ratio; no external verifier here takes caller-owned memory.
- Argon2id uses the OWASP shape (m=19 MiB, t=2, p=1). scrypt uses
log_n=14,r=8,p=1(16 MiB), so the unsplitpassword_hashingrun stays within one budget on the slowest runner. - Untimed checks accept the password from a call that reuses the row's state and reject a wrong one. Every record must also pass RustCrypto's verifier, and Argon2id records dryoc's.
ML-DSA groups use a 32-byte message. Keys are built outside timing unless a group imports them.
- Task groups:
sign/reuse-key-deterministic,sign/reuse-key-randomized,verify/reuse-key, andverify/import-key. They use an empty context, because aws-lc-rs takes none. Each library uses its best API, so how much each key caches is a design difference.- Verify rows parse the encoded signature inside timing.
import-keyalso imports identical public-key bytes. - Randomized rows take 32 bytes from AWS-LC's RNG in each call, in every library.
That group exists only where aws-lc-rs builds.
RustCrypto signs through
sign_internalwith the empty-context prefix, because its randomized API needs arand_corefeature that the benchmarks do not enable. rscrypto-compactdecodes its key in every call, as AWS-LC does.- AWS-LC has no deterministic signing API, so it is absent from the deterministic group.
- Each verify check runs the timed path. It must accept the signature, and reject a tampered and a malformed one.
- Verify rows parse the encoded signature inside timing.
- Equal key-depth groups:
encoded-keyandprepared-key, with an 18-byte context. Encoded-key rows decode the key in every call. Prepared-key rows cache the matrix and the transformed vectors. Inverify/prepared-key, RustCrypto's signature is decoded outside timing, whilerscryptodecodes inside. keygen/seed-encoded: turns a seed into the encoded public key and the expanded secret key. Generation, export, and destruction are timed.- There is no signing import group:
rscryptoand AWS-LC validate an imported secret key by recomputing the public key, and libcrux and RustCrypto do not. Such a group would compare validation policies.
.config/criterion.json supplies one configuration for every Criterion harness,
also for direct Cargo invocations: 20 samples, 100 ms warmup, 400 ms requested measurement time,
10,000 bootstrap resamples, 95% confidence, 5% significance, and a 1% noise threshold.
Benchmark groups must not override these settings.
warmup_ms=, measure_ms=, and sample_size= override the shared defaults for every selected case in that invocation.
Their BENCH_ environment variables have lower precedence than explicit arguments.
Boolean controls reject unknown values and empty strings.
These are bounded development defaults, not a promise of statistical precision.
Inspect the confidence intervals.
Repeat a focused selection when the uncertainty cannot support the claim you want to make.
For slow operations, Criterion can extend the requested measurement window to collect the requested
samples.
For example, Criterion estimated about 34 seconds per RustCrypto SLH-DSA-SHAKE-192s signing iteration
on RISC-V in run 39100888583.
The full comparison exceeded the ten-minute limit even at ten samples and 10 ms sampling windows.
Quick mode validates those cases with fewer iterations; it does not qualify the regular statistical campaign.
Criterion 0.8.2 retains two quick samples and can derive the second from the first for a slow operation.
Those estimates do not establish uncertainty or a performance winner.
argon2id includes small, OWASP, and parallel workloads.
No opt-in for expensive workloads is needed.
just bench runs the complete default catalog as a sequential campaign.
Each target gets a separate measurement run and result directory.
Unfiltered targets marked split: algorithms run one complete catalog operation at a time, just as they do in CI.
The first failure stops the campaign; earlier completed results remain available.
The campaign has no shared ten-minute deadline.
Each ordinary measurement run has 10 minutes for discovery, measurement, analysis,
and result verification.
Before measuring a target, the runner builds and discovers it in a separate invocation, as --list does.
That invocation has the 30-minute max_comparison_seconds limit.
It uses the same features and internal flags as the following measurement,
so the measurement finds its build current.
The cold Windows build of the auth target alone took longer than 10 minutes in Bench runs
39867778994 and 39880626790.
--list runs only the per-target invocations; a cold listing of many targets can take longer in total.
Cross-build preparation and runs of prepared binaries keep one bounded invocation.
just bench-structural and just profile use the same limit, including their builds.
The shared max_run_seconds setting can lower this ordinary limit; it cannot exceed 600 seconds.
The catalog gives 30 minutes to the 12 complete signing comparisons for SLH-DSA
and HashSLH-DSA with SHA-2 or SHAKE and the 128s, 192s, or 256s parameter sets.
It also gives 30 minutes to RSA-4096 key generation (rsa-4096-keygen):
its rscrypto row alone ran longer than four minutes on the RISC-V runner in Bench run 39463299690,
and the comparison adds two more implementations.
Select one operation without a filter, for example just bench slhdsa-shake-192s-sign.
The runner and compiled harness use the same resolved budget and retain every implementation
and sampling setting.
These exceptions use the catalog algorithm's max_run_seconds, bounded by max_comparison_seconds in .config/criterion.json;
there is no invocation option to raise the limit.
Quick runs, cross-build preparation, and filtered selections keep the ordinary limit.
Native campaigns and CI expand unfiltered umbrella selectors into separate operations
before resolving their budgets.
Filters keep the selected cases together within each target.
- Shutdown starts before the deadline. It keeps up to five seconds to save failed-run evidence, then stops the remaining child processes.
- A run that times out exits with status 124. Partial results are not a complete run.
- A plan whose requested sampling windows alone use up the budget is rejected before measurement. Analysis and slow operations can still make a smaller plan reach the deadline.
- Direct Cargo invocation limits each Criterion harness separately.
Use
just benchto run a selection across harnesses with separate build and measurement deadlines. Usejust bench-structuralfor the structural benchmark deadline. just bench-mldsa-tailandjust bench-provider-scorecardare separate tools with their own timeouts: 20 minutes per build and 60 per run for the ML-DSA tail, 10 minutes for each scorecard build and run.
A planned comparison must fit within this limit, including every repetition. Do not split an over-budget comparison across repeated invocations to bypass it. A comparison is one Criterion benchmark group: one operation of one algorithm, across its implementations.
A catalog target marked "split": "algorithms" runs one measurement per catalog algorithm, which never divides a group.
SLH-DSA uses 72 operation selectors: key generation, signing, verification,
and secret-key import for each pure parameter set,
plus signing and verification for each HashSLH-DSA parameter set.
Each invocation keeps the complete comparison for that operation.
The default auth schedule runs SLH-DSA after the other targets,
so a slow SLH-DSA comparison does not prevent PBKDF2, password hashing, or RSA from starting.
PBKDF2 uses it too: at the password-policy iteration count, one SHA-256 call takes about 1.25 s on the RISC-V runner,
extrapolated from its 1,000-iteration rows.
The auth target also splits by algorithm: its cold Windows build plus all 15 algorithms exceeded one budget
in Bench run 39091310295.
The invocations reuse the same compiled binary and preserve every case and sampling setting.
Every case of a split target must belong to exactly one of its algorithms; discovery fails otherwise.
RSA key generation has its own rsa_keygen target, separate from rsa verification, signing, and encryption.
It splits by key size: CI runs rsa-2048-keygen, rsa-3072-keygen and rsa-4096-keygen as separate invocations
in the auth family job. Its case IDs remain rsa-{bits}-keygen/....
If required evidence cannot fit, record that requirement as unmet. A smaller future
experiment needs a prospectively stated question and gates; it does not complete
an older, larger qualification plan. Preserve historical frozen plans and failed
captures unchanged; their old budgets do not authorize rerunning them.
Select cases by catalog name. Benchmark targets, algorithms, and groups share one name space,
and the catalog rejects a name that two of them would share.
Matching ignores case and punctuation, so password-hashing selects the password_hashing target.
| Name kind | Example | Selects |
|---|---|---|
| Target | sha2, auth, blake3_file |
Every case in one benchmark target. |
| Algorithm | sha256, ed25519, blake3-mixed |
The cases for one algorithm, inside its target. |
| Group | hashes, checksums, crc64, mlkem |
The algorithms and targets that the group lists. |
all |
all |
Every default target. Running with no name does the same. |
Default targets are the catalog's required Criterion targets.
A target with an external prerequisite is never a default target.
tls_connections needs OpenSSL 4.0.3 and is excluded from default runs.
tls-provider is its umbrella group for discovery.
Measure one tls-handshake-<credential> or scenario algorithm per invocation; the complete umbrella exceeds the run budget.
The TLS benchmark guide defines every family and its comparison boundary.
quic_connections has the same external prerequisite and is also excluded from default runs.
quic-provider lists its three selectors: quic-handshake, quic-packet, and quic-header.
Measure one selector per invocation.
The QUIC benchmark guide separates the QUIC TLS handshake,
packet protection and header protection.
filter=<pattern> limits the selected algorithms or targets.
For example, sha256 filter=rscrypto stays within SHA-256, but sha2 filter=rscrypto searches the whole SHA-2 target.
- Repeat
filter=<pattern>for more than one pattern. - Each build configuration is listed once.
- A lightweight invocation of the same executable matches all patterns with Criterion's regex engine, without building benchmark fixtures.
- The matched cases run as one measurement process for each build configuration. The harness reads the resolved case set from a file and applies an anchored, escaped union filter.
- A pattern that matches no cases fails before measurement.
- Patterns are passed exactly as written. Commas are regex characters, not separators. Quote each argument for your shell.
BENCH_FILTERsupplies one literal pattern, in addition to anyfilter=arguments.- Empty
filter=values are rejected. For an unfiltered run, omit the argument. - Positional selectors accept catalog names.
Use
filter=for raw regexes. - An exact algorithm name selects only that algorithm.
Use a group name, such as
crc64, to select more than one. blake2is a target name, so it includes all implementations and operations, including the dryoc one-shot and keyed cases.- SLH-DSA operation selectors include
slhdsa-shake-192s-keygen,slhdsa-shake-192s-sign,slhdsa-shake-192s-verify,slhdsa-shake-192s-import, and the correspondinghash-slhdsa-shake-192s-signandhash-slhdsa-shake-192s-verifynames. The existingslhdsa-shake-192sandhash-slhdsa-shake-192snames are umbrella groups that retain their complete pure and HashSLH-DSA workloads. The workflow expands those groups into separate operation runs; localjust benchcombines the selected cases into one invocation, so select one operation to use its separate budget.
Discover the actual cases before you choose a measurement scope:
just bench crc64-nvme --list
just bench sha2 --list
just bench blake3 --diag --list
just bench aead_kernels --list--list builds the selected configuration and lists its cases.
It does not measure, create a run, or copy baseline data.
It uses the same filters and case deduplication as measurement.
Each row shows its benchmark binary, the exact case name, and its work class:
ordinary: public operation and comparison workloads.expensive: high-cost workloads that the catalog declares, including password hashing, PBKDF2, and RSA private signing.diagnostic: internal components, backend experiments, and overhead probes.
The classes come from .config/benchmark-matrix.json.
They describe the intent of a workload, not its measured duration,
and they give no timing guarantee.
A diagnostic case can also be costly.
Classes do not block execution.
--diag (or diag=true) enables diag and the internal compiler cfg for the selected benchmark builds.
Some target configurations already need it.
The cases depend on the compiled features and the host capabilities.
Dedicated diagnostic targets, such as aead_kernels, need explicit selection.
all and runs with no name include only the default targets.
Run the narrowest useful case:
just bench sha2
just bench auth filter='^ecdsa-p256/'
just bench sha256 'filter=^sha256/rscrypto/\d+$' 'filter=^sha256/rscrypto/[0-9]{1,3}$'
just bench p256-ecdh
just bench p384-ecdh
just bench mlkemTarget names, including unfiltered sha2, run without a scope override.
- P-256 ECDH uses the
p256-ecdhselector. Exact profiling uses theauthcatalog target that owns it. Its rows compare caller-filled generation, public derivation, canonical SEC1 parsing, agreement with a parsed or an encoded peer key, and a TLS-shaped two-party roundtrip. - P-384 ECDH uses the
p384-ecdhselector, with caller-filled generation, public-derivation, parsing, and agreement rows.
The raw target results and the overview are the only performance record.
Each native campaign measurement prints its own result directory.
When RSCRYPTO_BENCH_RUN_ID is set and the selection needs multiple runs,
the runner appends the target or operation name to that ID.
For a baseline comparison, select the corresponding target or operation
and pass its completed result directory.
requests.jsonkeeps the resolved target and filter requests and the run budget.plan.jsonrecords, for each configuration: the selected cases, the Cargo command and artifact, the executable hash, the compatibility evidence, the effective settings, the baseline cases, the execution command, and the output location.- Raw results are under
criterion/<binary>-<configuration-id>/, in Criterion's directory layout. output.txtis the build, discovery, and measurement log.source.jsonandsource-state.jsonidentify the source files and the worktree.
The runner verifies all planned measurements once before it marks the run complete.
The shared environment collector separates build inputs from runtime controls,
including Rayon thread controls and CRC backend overrides.
For the auth benchmark on an AVX2-capable x86-64 host,
RSCRYPTO_BENCH_DISABLE_IFMA=1 removes IFMA from the detected capabilities before initialization.
This measures the production AVX2 Curve25519 fallback on IFMA hosts.
It does not change the separate Linux assembly paths.
Known runtime controls that are not set are recorded as explicit JSON nulls.
Benchmark plans and profile metadata carry the same compatibility evidence.
Criterion measures elapsed time.
just bench-structural uses Gungraun and Valgrind to count instructions and cache events on supported Linux hosts.
Those counts do not prove wall-clock speed.
After a benchmark shows a specific cost, inspect it:
just profile sha2 --list
just profile sha2 'sha256/rscrypto/64' 10
just profile blake3 --diag --list
just perf-codegen sha2 -- --asm <function>
just perf-llvm-lines sha2 -- --filter <pattern>- Profiling needs one exact case name.
--listbuilds the selected target and lists its cases without recording.- Capture checks that the name occurs exactly once, then runs that executable with only the resolved case selected.
- Unrelated workload groups skip fixture construction.
- The requested duration applies to that case. Process startup and profiler overhead add to the total elapsed time.
Each local Samply capture gets its own directory under target/profiles/.
It contains profile.json.gz, cases.json, metadata.json, a log, and source evidence.
- The metadata records the exact case, the executable path and SHA-256, the Cargo artifact description, the build and capture commands, compiler and tool versions, the build and runtime environment, and the capture outcome.
- The source evidence records input hashes, the revision, and the worktree status.
- Keep the matching executable and its symbols when you investigate a saved profile.
Transferred CI captures instead keep the verified input bundle, raw perf.data, the native perf-report.txt,
host and capability facts, metadata, status, and an outer sealed manifest.
Only a complete sampled capture exits successfully.
A partial, unavailable, or failed capture stays downloadable and keeps the workflow from passing.
Benchmarking and profiling share the Cargo command and the CPU-flag policy.
- Both use the
benchprofile. It inherits the release optimization settings and keeps debug symbols, without stripping. - Explicit
RUSTFLAGSorCARGO_ENCODED_RUSTFLAGStake precedence. Otherwise, local macOS runs use-C target-cpu=native. Match these flags, the target, and the Cargo features when you compare with a deployment build. - A target with catalog
rustflags, such asxxh3_v4, appends them to those flags for its own build only. - Each target uses its explicit catalog features with Cargo defaults disabled. Algorithm selectors, raw filters, and multi-target selection do not change this.
- Each distinct target configuration is built once, before its selected cases run.
- Profiling and code inspection use the same catalog target configuration.
--diagenables the same feature and internal compiler cfg in each command. - The
blake3,blake3_file, and password-hashing targets enableparallel, where their workloads use it.blake3_serialbuilds the same binary without it, for the single-thread BLAKE3 comparisons. - Cargo ignores the panic setting for benchmarks,
so the release profile's
panic = "abort"stays a difference (Cargo profiles).