Measurements¶
The design notes state each measured claim once and link here. Each section gives the machine, the compiler and its flags, and where the full results and the code that produced them are kept.
The bit-permutation kernels¶
benchmark/ times each kernel and the alternatives it beat, in cycles per independent call on a 64-bit block
of stated density, the harness that feeds each call included. benchmark/results/ keeps each run with
the machine, compiler, flags and commit it names, and CONTRIBUTING.md gives the
command.
From the Cascade Lake run, at -march=native -O3, with a cost per bit
taken as the slope from the sparsest row of its table to the densest:
| Kernel | Cost | GCC 15 | Clang 22 |
|---|---|---|---|
PEXT, PDEP |
per call | 1.7 | 1.7 to 2.0 |
block_compress_by_rank |
per selected bit | 3.2 | 2.5 |
block_expand_by_walk |
per mask bit | 3.0 | 2.5 |
block_expand_by_select |
per source bit | 7.4 | 7.0 |
std::countr_zero of PDEP |
per call | 3.0 | 2.0 |
block_select_by_broadword |
per call | 12.1 | 11.7 |
| the harness alone | per call | 2.1 | 1.8 |
With two source ones, block_expand_by_select beats the walk from about nine mask ones up. The
alternatives measured and left out cost, on the same run: the mask walk with an if on each bit,
which both compilers turn into a conditional move rather than a branch, 1.75 times the rank kernel
to compress at half density, since it pays for every mask bit rather than every selected one, and
to expand as much as the masked walk with Clang and less with GCC, 2.1 cycles a mask bit against
3.0; Hacker's Delight's parallel suffix, a flat 70 cycles to compress and 77 to expand; and
Wegner's zp7, a flat 52 to 58 and 61 to 66 without PCLMULQDQ, and 25 to 26 and 34 with it.
Secret-dependent control flow¶
Whether a kernel's control flow or memory addresses depend on secret data is checked per kernel and per secret
argument in test/constant_time/, with its results. Valgrind's memcheck,
with the secret marked undefined, reports every branch and address computed from it, the same for
GCC 15 and Clang 22 with and without -march=x86-64-v3:
| Kernel | Secret | Memcheck |
|---|---|---|
block_reverse_by_swaps |
the word | clean |
block_expand_by_walk |
the source, the mask public | clean |
block_expand_by_walk |
the mask | a branch: one trip per mask one |
block_compress_by_rank |
the source | a branch: one trip per selected bit |
block_expand_by_select |
the source | a branch, and the table lookup |
block_select_by_broadword |
the word and the rank | no branch, but one address from them: select_in_byte |
PEXT, PDEP |
the source or the mask | clean |
So only the byte reversal, the walk over a public mask, and the two instructions are free of
secret-dependent branches and addresses. The select never branches, yet indexes a table by the
secret, which a cache-timing attacker can observe, and the loops branch on no bit of the value but
run a number of times that depends on it. Memcheck says nothing about time itself: the instructions'
own latency can depend on their operands, as uops.info measures for
PEXT (R64, R64, R64) on Zen 2.