pqc-benchmark report

Post-quantum vs classical cryptography measured through one OpenSSL EVP path. 70 measured result(s) across 2 run file(s): mac-arm64, mac-arm64-kpqc.

13 of 70 measured algorithms are partially measured: one or more timed operations failed (sign @ 0 B, verify @ 0 B) and are excluded rather than shown as zero. Rows carry a partial tag naming the failed operations; all other cells in those rows are valid.

Single machine: Apple M3 Max (macOS/arm64), OpenSSL 3.6.3, 2 run files (2026-07-24)

How every algorithm compares

Every measured algorithm ranked on each metric, with no single baseline and no faster/slower verdict. Key exchange & KEM on the left, signatures on the right; each row is one metric. The reply row is a KEM ciphertext or a DH ephemeral public key, and wire total counts both protocol messages (the apples-to-apples size). Pick a single NIST level for a like-for-like comparison. Bar length is proportional to the value by default; the scale toggle switches every card to log-scaled bars, where equal steps in length are equal ratios, which spreads out the slow tail. The exact number is labeled on each bar either way.

How the three KEM operations map to OpenSSL EVP calls

The three KEM operations map to different OpenSSL EVP calls depending on the family. Native KEMs (ML-KEM, HQC, FrodoKEM, BIKE, and RSA via RSASVE) use the encapsulate/decapsulate API directly. ECDH and X25519 are not KEMs in OpenSSL, so they are mapped in as DHKEM (RFC 9180) built from keygen + derive. Same roles, so they share the KEM axes:

Step Native KEM (ML-KEM, HQC, RSA via RSASVE) ECDH / X25519 (DHKEM, RFC 9180)
Keygen EVP_PKEY_keygen
sample the private key, compute the public key from the scheme trapdoor (lattice t = A·s + e, code, or RSA modulus)
EVP_PKEY_keygen
pick a scalar d, compute the recipient point d·G
Encapsulate EVP_PKEY_encapsulate
draw a random shared secret, wrap it under the recipient public key to make the ciphertext
EVP_PKEY_keygen + EVP_PKEY_derive
generate an ephemeral keypair e, compute e·P; the ephemeral public key e·G is the ciphertext. The ephemeral keygen is timed here, which is why ECDH encapsulate is slower than its own keygen.
Decapsulate EVP_PKEY_decapsulate
recover the shared secret from the ciphertext with the private key
EVP_PKEY_derive
compute d·(e·G), the same shared point d·e·G

Keygen + encapsulate + decapsulate together are one full two-message exchange, split by who pays each cost, not double-counted.

KEM / key exchange

cross-provider (ref impl) PQC▲ beats fastest classical baseline in its group
Italicized rows on a hatched background are the classical (pre-quantum) algorithms, listed in each NIST level as the baseline the PQC rows are compared against. Timing cells show median time (lower is faster) and ops/s in parentheses (higher is faster). Cell shading compares within each NIST level: faster slower. Size cells: fewer bytes is smaller (shaded the same way). PK/SK/CT/Sig are raw component sizes (the standardized encodings for PQC schemes; for RSA the private size is approximated as the modulus length); where a run recorded them, the DER-serialized size (SubjectPublicKeyInfo / PKCS#8) is shown beneath the raw value. Wire columns sum raw sizes, the values carried in TLS key shares; keys carried inside certificates occupy the DER size instead. A failed cell means that operation errored during the run and has no valid samples; a partial tag on an algorithm means some of its operations failed that way (hover it for which) while the remaining cells are valid; a small n= superscript marks medians from few samples (slow ops that exhausted their time budget).

Signatures

cross-provider (ref impl) PQC▲ beats fastest classical baseline in its group
Italicized rows on a hatched background are the classical (pre-quantum) algorithms, listed in each NIST level as the baseline the PQC rows are compared against. Timing cells show median time (lower is faster) and ops/s in parentheses (higher is faster). Cell shading compares within each NIST level: faster slower. Size cells: fewer bytes is smaller (shaded the same way). PK/SK/CT/Sig are raw component sizes (the standardized encodings for PQC schemes; for RSA the private size is approximated as the modulus length); where a run recorded them, the DER-serialized size (SubjectPublicKeyInfo / PKCS#8) is shown beneath the raw value. Wire columns sum raw sizes, the values carried in TLS key shares; keys carried inside certificates occupy the DER size instead. A failed cell means that operation errored during the run and has no valid samples; a partial tag on an algorithm means some of its operations failed that way (hover it for which) while the remaining cells are valid; a small n= superscript marks medians from few samples (slow ops that exhausted their time budget).

Symmetric context

Secondary section: hashes and AEADs measured through the same EVP path, for a Grover-era reference point.

Throughput is MB/s; its bar is linear, scaled to the fastest row shown (longer = faster). Median is the measured value behind it: the median wall-clock time of one call (one digest, one encrypt) on a buffer of that size; throughput = buffer size / median. Per-call time spans four orders of magnitude across buffer sizes, so its bar is log-scaled: an equal step in length is roughly 10x in time, and longer = slower.
Why a hash can beat AES-GCM at 16 B and lose heavily at 64 KB

The two ends of this table measure different things. At large buffers the numbers reflect the core primitive: on hardware with dedicated instructions, AES blocks pipeline in parallel while a hash compression function is one serial dependency chain, so bulk AES-GCM throughput typically runs several times past SHA-256.

At the smallest buffer the payload work is negligible and the number is dominated by fixed per-call cost, which is intrinsically higher for an AEAD seal than for a digest. One digest call initializes eight words of state, runs one compression, and is done. One GCM seal sets the nonce and derives the initial counter block, encrypts its single block, then closes out GHASH with the mandatory length block and encrypts the authentication tag, through a heavier EVP cipher-context setup path. That bookkeeping is the same regardless of payload, and at 16 B it is most of the call, which is how a hash can post a faster small-message time than an AEAD it loses to in bulk.

The same effect stacks for a cipher without dedicated instructions on the host CPU (SM4-GCM on most non-Chinese silicon): GCM overhead plus a software block cipher. This is why each row is measured at several buffer sizes: small buffers rank per-call overhead, large buffers rank the primitive.

Appendix: skipped / error results

Records that did not reach status: ok this run (missing provider, build tier not present, or a failed correctness gate).

Standards

Published specification for each algorithm family present in this run, with its NIST PQC process history where applicable. Links open in a new tab.

AESFIPS 197, SP 800-38D (GCM)
AIMerKpqC (Korea)
Korea KpqC competition winner (2025); not part of the NIST PQC process.
BIKEnot standardized (research / competition candidate)
NIST PQC round-4 KEM candidate; not selected (round 4 closed Mar 2025).
CROSSnot standardized (research / competition candidate)
NIST additional-signatures on-ramp, round 2 (2024); not yet selected.
ChaCha20RFC 8439
ECDHFIPS 186-5
ECDSAFIPS 186-5
Ed25519RFC 8032
Ed448RFC 8032
FalconFIPS 206 (draft)
NIST PQC round-3 finalist; selected Jul 2022; FIPS 206 (FN-DSA) in draft.
FrodoKEMISO/IEC 18033-2 Amd 2
NIST PQC round-3 alternate; not advanced — continued on the ISO track.
HAETAEKpqC (Korea)
Korea KpqC competition winner (2025); not part of the NIST PQC process.
HQCNIST selection (2025)
NIST PQC round-4 KEM; selected Mar 2025 as the fifth standardized KEM.
MAYOnot standardized (research / competition candidate)
NIST additional-signatures on-ramp, round 2 (2024); not yet selected.
ML-DSAFIPS 204
NIST PQC round-3 finalist (CRYSTALS-Dilithium); selected Jul 2022.
ML-KEMFIPS 203
NIST PQC round-3 finalist (CRYSTALS-Kyber); selected for standardization Jul 2022.
NTRU+KpqC (Korea)
Korea KpqC competition winner (2025); not part of the NIST PQC process.
RSAFIPS 186-5
SHA-FIPS 180-4
SHA3FIPS 202
SLH-DSAFIPS 205
NIST PQC round-3 alternate (SPHINCS+); selected Jul 2022.
SM2ISO/IEC 14888-3, RFC 8998 (TLS)
SM3ISO/IEC 10118-3
SM4ISO/IEC 18033-3, RFC 8998 (TLS)
SMAUG-TKpqC (Korea)
Korea KpqC competition winner (2025); not part of the NIST PQC process.
SecP256r1MLKEM768draft-ietf-tls-ecdhe-mlkem, FIPS 203
X25519RFC 7748
X25519MLKEM768draft-ietf-tls-ecdhe-mlkem, FIPS 203
X448MLKEM1024draft-ietf-tls-ecdhe-mlkem, FIPS 203

Referenced building block, not benchmarked as a standalone row: the SHAKE extendable-output function (FIPS 202 (SHAKE)), used to expand the public seed into the module/lattice matrix inside ML-KEM and FrodoKEM. The AES rows run in GCM mode, whose specification is linked alongside the AES block cipher above.

Standardized but not benchmarked: Classic McEliece (ISO/IEC 18033-2 Amd 2, classic.mceliece.org), a NIST PQC round-4 KEM candidate not selected by NIST (round 4 closed Mar 2025) but standardized in the same ISO/IEC 18033-2 amendment as FrodoKEM and recommended by BSI (Germany). No OpenSSL provider currently exposes it (oqs-provider carries the liboqs implementation but has no provider mapping for it), and this suite only measures algorithms reachable through the OpenSSL EVP call path. It will be added when a provider exposes it.

Run metadata

mac-arm64

Source filemac-arm64-20260724.json
Host slugmac-arm64
CPUApple M3 Max
Archarm64
OSDarwin 25.5.0
Cores (physical)14
Compilerclang 21.0.0 (clang-2100.1.1.101)
OpenSSLOpenSSL 3.6.3 9 Jun 2026
Providersdefault=3.6.3, oqsprovider=0.11.0, kpqc=absent
liboqsunknown
Iterations / warmup1000 / 100
TimerCLOCK_UPTIME_RAW
Git commitfc41123
Timestamp2026-07-24T02:11:26Z

mac-arm64-kpqc

Source filemac-arm64-kpqc-20260724.json
Host slugmac-arm64-kpqc
CPUApple M3 Max
Archarm64
OSDarwin 25.5.0
Cores (physical)14
Compilerclang 21.0.0 (clang-2100.1.1.101)
OpenSSLOpenSSL 3.6.3 9 Jun 2026
Providersdefault=3.6.3, oqsprovider=absent, kpqc=0.9.1-dev
liboqsabsent
Iterations / warmup1000 / 100
TimerCLOCK_UPTIME_RAW
Git commitfc41123
Timestamp2026-07-24T02:23:07Z