Benchmarks
This is a portable pure-OCaml implementation. The numbers below are a baseline to reason about and to reproduce.
dune exec --profile release bench/bench_ascon.exe
Measure on your own machine. These figures come from one run on one laptop. Cryptographic throughput is sensitive to the compiler, the runtime, and the profile; comparisons are only meaningful between results measured on the same hardware.
Start with the permutation
Everything here is the same 320-bit permutation applied over and over, so one number sets the ceiling for all four constructions:
| Time per permutation | 447 ns |
|---|---|
| Permutations per second | 2.24 million |
| Time per round | 37 ns |
The rest follows from the standard's parameters. Hashing absorbs 8
bytes per p[12], which is 1.5 rounds per byte; AEAD
processes 16 bytes per p[8], which is 0.5. At 37 ns per
round that predicts roughly 18 MB/s for the hash family and 54 MB/s for
AEAD — and that is very close to what the full constructions
actually measure. There is almost nothing in the hot loop but the
permutation.
Throughput and allocation
| Operation | 1 KiB | 1 MiB | Allocated words/op at 1 KiB |
|---|---|---|---|
| AEAD128 encrypt | 47.4 MB/s | 51.0 MB/s | 15,898 |
| AEAD128 decrypt | 48.4 MB/s | 42.4 MB/s | 15,898 |
| Hash256 | 18.8 MB/s | 19.9 MB/s | 44,254 |
| XOF128, 32-byte output | 19.2 MB/s | 18.6 MB/s | 44,254 |
| CXOF128, 32-byte output | 17.2 MB/s | 19.2 MB/s | 45,295 |
AEAD runs about two and a half times faster per byte than the hash
family. That gap is the standard's, not the implementation's: the
parameters alone predict 3×, and what is missing is the two extra
p[12] permutations and the associated-data blocks that
every AEAD message pays regardless of its length.
Where the allocation goes
The allocation column looks alarming until you divide it by the number
of permutation rounds each operation performs. A 1 KiB
Hash256 digest runs 134 permutations, or 1,608 rounds; a 1
KiB AEAD encryption runs 552 rounds. Both land at roughly
28 words per round.
That is not the persistent-context API being wasteful. Absorbing copies
the five-word state once per call, not once per block — a few
dozen words. The rest is OCaml boxing Int64 intermediates
inside the round function, which computes ten or so of them per round
and stores five back into the state record. It is short-lived minor-heap
garbage that the collector handles cheaply, but on an allocation-averse
target it is the number to know.
| Operation on 1 KiB | Rounds | Words/round |
|---|---|---|
| Hash256 digest | 134 × p[12] = 1,608 |
27.5 |
| AEAD128 encrypt | 66 × p[8] + 2 × p[12] = 552 |
28.8 |
The Hash256 count is one p[12] to initialize, 128 to
absorb the message, one for the padded final block, and four to squeeze
32 bytes. The AEAD count is 64 payload blocks plus two associated-data
blocks at p[8], with p[12] at initialization
and finalization.
The full matrix
The committed benchmark reports every construction at 0, 8, 16, 32, 64,
256, 4,096, 16,384, and 1,048,576 bytes as well as the two sizes above,
printing nanoseconds per operation, throughput, and allocated words for
each. The small sizes are the interesting ones for a lightweight
primitive: at 8 or 16 bytes the fixed cost of initialization and
finalization dominates completely, and throughput in MB/s stops being a
useful way to read the result. Compare ns/op there
instead.
What these numbers are not
- Not a comparison with C. The official C implementations, especially with hardware-assisted or bitsliced builds, are substantially faster. Nothing here competes with them.
- Not constant-time evidence. Throughput says nothing about side channels. See the security notes for what is and is not claimed.
-
Not stable across compilers. The allocation profile
in particular depends on how much
Int64unboxing the compiler manages. Re-measure after changing OCaml versions or build profile. - Not a regression gate. The benchmark is a developer tool, run by hand; it is not part of CI.
The benchmark source is
bench/bench_ascon.ml.
It compacts the heap and samples Gc.quick_stat around each
measurement, and scales its iteration count so that small sizes still
get a meaningful sample.