keccak-fast.c

Minimal SIMD keccak implementation
git clone git://git.finwo.net/lib/keccak-fast.c
Log | Files | Refs | README | LICENSE

commit 51be4c74a70bfdcfa108768dde7ff034d782d387
parent 8412665afd9d5d9140642eea48055b43387d1a2b
Author: finwo <finwo@pm.me>
Date:   Sat, 10 Oct 2026 17:12:45 +0200

named-register permutation with fused theta and 4x round unroll

Diffstat:
MREADME.md | 16+++++++++-------
Msrc/backend/scalar-impl.h | 66+-----------------------------------------------------------------
Asrc/backend/scalar-perm.h | 206+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 216 insertions(+), 72 deletions(-)

diff --git a/README.md b/README.md @@ -42,12 +42,13 @@ Backends register from constructors; the highest available priority wins `kf_batch_name()` report the active ones. `KECCAK_FAST_NO_BMI`, `KECCAK_FAST_NO_AVX2` and `KECCAK_FAST_NO_AVX512` drop those backends. -The scalar backend is the donor, pinned to the x86-64 baseline +The scalar backend is pinned to the x86-64 baseline (`no-avx,no-avx2,no-avx512f,no-bmi,no-bmi2`) so a consumer's `-march=native` -cannot vectorize it or emit BMI. The round loop is unrolled 4x, which beats -both no unroll and a full 24x unroll. `scalar+bmi` is the same permutation with -`bmi` enabled, which turns chi's `not`+`and` into `andn`; gcc also gets `rorx` -for the rotates. About 11% faster than the donor. +cannot vectorize it or emit BMI. Its permutation keeps the 25 lanes in named +locals, fuses theta into the previous round's chi, and unrolls the round loop +4x; that beats both no unroll and a full 24x unroll. `scalar+bmi` is the same +permutation with `bmi` enabled, which turns chi's `not`+`and` into `andn`; gcc +also gets `rorx` for the rotates. About 20% faster than the donor. ## Dev @@ -61,6 +62,7 @@ rejected), `BENCH_CPU=<n>` selects the core. ## License -See `LICENSE.md`. The scalar backend is derived from `coruus/keccak-tiny` -(David Leon Gil, CC0). +See `LICENSE.md`. The sponge is derived from `coruus/keccak-tiny` (David Leon +Gil, CC0); the scalar permutation follows XKCP's public-domain `opt64` +structure. diff --git a/src/backend/scalar-impl.h b/src/backend/scalar-impl.h @@ -17,16 +17,6 @@ static inline int memset_s(void *dest, size_t destsz, int ch, size_t count) { } #endif -static const uint8_t rho[24] = { - 1, 3, 6, 10, 15, 21, - 28, 36, 45, 55, 2, 14, - 27, 41, 56, 8, 25, 43, - 62, 18, 39, 61, 20, 44}; -static const uint8_t pi[24] = { - 10, 7, 11, 17, 18, 3, - 5, 16, 8, 21, 24, 4, - 15, 23, 19, 13, 12, 2, - 20, 14, 22, 9, 6, 1}; static const uint64_t RC[24] = { 1ULL, 0x8082ULL, 0x800000000000808aULL, 0x8000000080008000ULL, 0x808bULL, 0x80000001ULL, 0x8000000080008081ULL, 0x8000000000008009ULL, @@ -36,62 +26,8 @@ static const uint64_t RC[24] = { 0x8000000080008081ULL, 0x8000000000008080ULL, 0x80000001ULL, 0x8000000080008008ULL}; #define rol(x, s) (((x) << s) | ((x) >> (64 - s))) -#define REPEAT6(e) e e e e e e -#define REPEAT24(e) REPEAT6(e e e e) -#define REPEAT5(e) e e e e e -#define FOR5(v, s, e) \ - v = 0; \ - REPEAT5(e; v += s;) - -/* Round loop unrolled 4x, after armed-keccak's roundx4: enough straight-line - work for the scheduler without the register spills a full 24x unroll causes. */ -#ifndef KF_UNROLL -#define KF_UNROLL 4 -#endif - -#define KF_STR_(x) #x -#define KF_STR(x) KF_STR_(x) -#if KF_UNROLL > 0 && defined(__clang__) -#define KF_UNROLL_PRAGMA _Pragma(KF_STR(clang loop unroll_count(KF_UNROLL))) -#elif KF_UNROLL > 0 && defined(__GNUC__) -#define KF_UNROLL_PRAGMA _Pragma(KF_STR(GCC unroll KF_UNROLL)) -#else -#define KF_UNROLL_PRAGMA -#endif - -#define KF_ROUNDS(FIRST) \ - uint64_t *a = (uint64_t *)state; \ - uint64_t b[5] = {0}; \ - uint64_t t = 0; \ - uint8_t x, y; \ - KF_UNROLL_PRAGMA \ - for (int i = (FIRST); i < 24; i++) { \ - /* theta */ \ - FOR5(x, 1, b[x] = 0; FOR5(y, 5, b[x] ^= a[x + y];)) \ - FOR5(x, 1, FOR5(y, 5, a[y + x] ^= b[(x + 4) % 5] ^ rol(b[(x + 1) % 5], 1);)) \ - /* rho and pi */ \ - t = a[1]; \ - x = 0; \ - REPEAT24(b[0] = a[pi[x]]; \ - a[pi[x]] = rol(t, rho[x]); \ - t = b[0]; \ - x++;) \ - /* chi */ \ - FOR5(y, 5, \ - FOR5(x, 1, b[x] = a[y + x];) \ - FOR5(x, 1, a[y + x] = b[x] ^ ((~b[(x + 1) % 5]) & b[(x + 2) % 5]);)) \ - /* iota */ \ - a[0] ^= RC[i]; \ - } - -static inline void keccakf(void *state) { - KF_ROUNDS(0) -} - -static inline void keccak12(void *state) { - KF_ROUNDS(12) -} +#include "scalar-perm.h" #define _(S) do { S } while (0) #define FOR(i, ST, L, S) \ diff --git a/src/backend/scalar-perm.h b/src/backend/scalar-perm.h @@ -0,0 +1,206 @@ +#ifndef FINWO_SCALAR_PERM_H +#define FINWO_SCALAR_PERM_H + +/* Keccak-p[1600] with the 25 lanes held in named locals rather than an indexed + array, so the scheduler can keep them in registers; theta is fused into the + previous round's chi (prepareTheta) and the round loop is unrolled 4x. */ + +#define KFP_LOAD(P, S) \ + P##0 = S[0]; \ + P##1 = S[1]; \ + P##2 = S[2]; \ + P##3 = S[3]; \ + P##4 = S[4]; \ + P##5 = S[5]; \ + P##6 = S[6]; \ + P##7 = S[7]; \ + P##8 = S[8]; \ + P##9 = S[9]; \ + P##10 = S[10]; \ + P##11 = S[11]; \ + P##12 = S[12]; \ + P##13 = S[13]; \ + P##14 = S[14]; \ + P##15 = S[15]; \ + P##16 = S[16]; \ + P##17 = S[17]; \ + P##18 = S[18]; \ + P##19 = S[19]; \ + P##20 = S[20]; \ + P##21 = S[21]; \ + P##22 = S[22]; \ + P##23 = S[23]; \ + P##24 = S[24]; \ + +#define KFP_STORE(S, P) \ + S[0] = P##0; \ + S[1] = P##1; \ + S[2] = P##2; \ + S[3] = P##3; \ + S[4] = P##4; \ + S[5] = P##5; \ + S[6] = P##6; \ + S[7] = P##7; \ + S[8] = P##8; \ + S[9] = P##9; \ + S[10] = P##10; \ + S[11] = P##11; \ + S[12] = P##12; \ + S[13] = P##13; \ + S[14] = P##14; \ + S[15] = P##15; \ + S[16] = P##16; \ + S[17] = P##17; \ + S[18] = P##18; \ + S[19] = P##19; \ + S[20] = P##20; \ + S[21] = P##21; \ + S[22] = P##22; \ + S[23] = P##23; \ + S[24] = P##24; \ + +#define KFP_INIT_C(SRC) \ + c0 = SRC##0 ^ SRC##5 ^ SRC##10 ^ SRC##15 ^ SRC##20; \ + c1 = SRC##1 ^ SRC##6 ^ SRC##11 ^ SRC##16 ^ SRC##21; \ + c2 = SRC##2 ^ SRC##7 ^ SRC##12 ^ SRC##17 ^ SRC##22; \ + c3 = SRC##3 ^ SRC##8 ^ SRC##13 ^ SRC##18 ^ SRC##23; \ + c4 = SRC##4 ^ SRC##9 ^ SRC##14 ^ SRC##19 ^ SRC##24; \ + +#define KFP_R(SRC, DST, RCIDX) \ + d0 = c4 ^ rol(c1, 1); \ + d1 = c0 ^ rol(c2, 1); \ + d2 = c1 ^ rol(c3, 1); \ + d3 = c2 ^ rol(c4, 1); \ + d4 = c3 ^ rol(c0, 1); \ + SRC##0 ^= d0; \ + b0 = SRC##0; \ + SRC##6 ^= d1; \ + b1 = rol(SRC##6, 44); \ + SRC##12 ^= d2; \ + b2 = rol(SRC##12, 43); \ + SRC##18 ^= d3; \ + b3 = rol(SRC##18, 21); \ + SRC##24 ^= d4; \ + b4 = rol(SRC##24, 14); \ + DST##0 = b0 ^ ((~b1) & b2); \ + DST##1 = b1 ^ ((~b2) & b3); \ + DST##2 = b2 ^ ((~b3) & b4); \ + DST##3 = b3 ^ ((~b4) & b0); \ + DST##4 = b4 ^ ((~b0) & b1); \ + DST##0 ^= RC[RCIDX]; \ + c0 = DST##0; \ + c1 = DST##1; \ + c2 = DST##2; \ + c3 = DST##3; \ + c4 = DST##4; \ + SRC##3 ^= d3; \ + b5 = rol(SRC##3, 28); \ + SRC##9 ^= d4; \ + b6 = rol(SRC##9, 20); \ + SRC##10 ^= d0; \ + b7 = rol(SRC##10, 3); \ + SRC##16 ^= d1; \ + b8 = rol(SRC##16, 45); \ + SRC##22 ^= d2; \ + b9 = rol(SRC##22, 61); \ + DST##5 = b5 ^ ((~b6) & b7); \ + DST##6 = b6 ^ ((~b7) & b8); \ + DST##7 = b7 ^ ((~b8) & b9); \ + DST##8 = b8 ^ ((~b9) & b5); \ + DST##9 = b9 ^ ((~b5) & b6); \ + c0 ^= DST##5; \ + c1 ^= DST##6; \ + c2 ^= DST##7; \ + c3 ^= DST##8; \ + c4 ^= DST##9; \ + SRC##1 ^= d1; \ + b10 = rol(SRC##1, 1); \ + SRC##7 ^= d2; \ + b11 = rol(SRC##7, 6); \ + SRC##13 ^= d3; \ + b12 = rol(SRC##13, 25); \ + SRC##19 ^= d4; \ + b13 = rol(SRC##19, 8); \ + SRC##20 ^= d0; \ + b14 = rol(SRC##20, 18); \ + DST##10 = b10 ^ ((~b11) & b12); \ + DST##11 = b11 ^ ((~b12) & b13); \ + DST##12 = b12 ^ ((~b13) & b14); \ + DST##13 = b13 ^ ((~b14) & b10); \ + DST##14 = b14 ^ ((~b10) & b11); \ + c0 ^= DST##10; \ + c1 ^= DST##11; \ + c2 ^= DST##12; \ + c3 ^= DST##13; \ + c4 ^= DST##14; \ + SRC##4 ^= d4; \ + b15 = rol(SRC##4, 27); \ + SRC##5 ^= d0; \ + b16 = rol(SRC##5, 36); \ + SRC##11 ^= d1; \ + b17 = rol(SRC##11, 10); \ + SRC##17 ^= d2; \ + b18 = rol(SRC##17, 15); \ + SRC##23 ^= d3; \ + b19 = rol(SRC##23, 56); \ + DST##15 = b15 ^ ((~b16) & b17); \ + DST##16 = b16 ^ ((~b17) & b18); \ + DST##17 = b17 ^ ((~b18) & b19); \ + DST##18 = b18 ^ ((~b19) & b15); \ + DST##19 = b19 ^ ((~b15) & b16); \ + c0 ^= DST##15; \ + c1 ^= DST##16; \ + c2 ^= DST##17; \ + c3 ^= DST##18; \ + c4 ^= DST##19; \ + SRC##2 ^= d2; \ + b20 = rol(SRC##2, 62); \ + SRC##8 ^= d3; \ + b21 = rol(SRC##8, 55); \ + SRC##14 ^= d4; \ + b22 = rol(SRC##14, 39); \ + SRC##15 ^= d0; \ + b23 = rol(SRC##15, 41); \ + SRC##21 ^= d1; \ + b24 = rol(SRC##21, 2); \ + DST##20 = b20 ^ ((~b21) & b22); \ + DST##21 = b21 ^ ((~b22) & b23); \ + DST##22 = b22 ^ ((~b23) & b24); \ + DST##23 = b23 ^ ((~b24) & b20); \ + DST##24 = b24 ^ ((~b20) & b21); \ + c0 ^= DST##20; \ + c1 ^= DST##21; \ + c2 ^= DST##22; \ + c3 ^= DST##23; \ + c4 ^= DST##24; \ + +#define KFP_R2(SRC, DST, I) KFP_R(SRC, DST, I) KFP_R(DST, SRC, I + 1) +#define KFP_R4(SRC, DST, I) KFP_R2(SRC, DST, I) KFP_R2(SRC, DST, I + 2) +#define KFP_R12(SRC, DST, I) \ + KFP_R4(SRC, DST, I) KFP_R4(SRC, DST, I + 4) KFP_R4(SRC, DST, I + 8) + +#define KFP_DECL(P) \ + uint64_t P##0, P##1, P##2, P##3, P##4, P##5, P##6, P##7, P##8, P##9, \ + P##10, P##11, P##12, P##13, P##14, P##15, P##16, P##17, P##18, P##19, \ + P##20, P##21, P##22, P##23, P##24 + +#define KFP_BODY(ROUNDS) \ + uint64_t *s = (uint64_t *)state; \ + KFP_DECL(a); \ + KFP_DECL(e); \ + KFP_DECL(b); \ + uint64_t c0, c1, c2, c3, c4, d0, d1, d2, d3, d4; \ + KFP_LOAD(a, s) \ + KFP_INIT_C(a) \ + ROUNDS \ + KFP_STORE(s, a) + +static inline void keccakf(void *state) { + KFP_BODY(for (int i = 0; i < 24; i += 4) { KFP_R4(a, e, i) }) +} + +static inline void keccak12(void *state) { + KFP_BODY(for (int i = 12; i < 24; i += 4) { KFP_R4(a, e, i) }) +} + +#endif /* FINWO_SCALAR_PERM_H */