commit 721000be4b2df7f3d084e9dce86bb229036393dc
parent 812b92a5f8c0598e3ede2652ddfe7bf0ea3e5798
Author: finwo <finwo@pm.me>
Date: Sat, 10 Oct 2026 16:12:19 +0200
Add scalar+bmi backend
Diffstat:
6 files changed, 223 insertions(+), 137 deletions(-)
diff --git a/README.md b/README.md
@@ -28,18 +28,22 @@ dep add finwo/keccak-fast
## Backends
-| backend | one-shot | batch |
-| ------- | -------- | ----- |
-| scalar | yes | — |
-| AVX2 | no | x4 |
+| backend | one-shot | batch |
+| ---------- | -------- | ----- |
+| scalar | yes | — |
+| scalar+bmi | yes | — |
+| AVX2 | no | x4 |
Backends register from constructors; the highest available priority wins
-(scalar 0, avx2 20, avx512 30). `kf_backend_name()` and `kf_batch_name()`
-report the active ones. `KECCAK_FAST_NO_AVX2` drops the AVX2 backend.
+(scalar 0, scalar+bmi 10, avx2 20, avx512 30). `kf_backend_name()` and
+`kf_batch_name()` report the active ones. `KECCAK_FAST_NO_BMI` and
+`KECCAK_FAST_NO_AVX2` drop those backends.
The scalar backend is the donor, pinned to the x86-64 baseline
-(`no-avx,no-avx2,no-avx512f`) so a consumer's `-march=native` cannot vectorize
-it (clang runs 1.9x slower when it does).
+(`no-avx,no-avx2,no-avx512f,no-bmi,no-bmi2`) so a consumer's `-march=native`
+cannot vectorize it or emit BMI. `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.
## Dev
diff --git a/export.mk b/export.mk
@@ -1,3 +1,4 @@
SRC+={{module.dirname}}/src/keccak-fast.c
SRC+={{module.dirname}}/src/backend/scalar.c
+SRC+={{module.dirname}}/src/backend/scalar_bmi.c
SRC+={{module.dirname}}/src/backend/avx2.c
diff --git a/src/backend/scalar-impl.h b/src/backend/scalar-impl.h
@@ -0,0 +1,140 @@
+#ifndef FINWO_SCALAR_IMPL_H
+#define FINWO_SCALAR_IMPL_H
+
+#include <stddef.h>
+#include <stdint.h>
+#include <string.h>
+
+#define KF_CAT2(a, b) a##b
+#define KF_CAT(a, b) KF_CAT2(a, b)
+#define KF_FN(name) KF_CAT(KF_PREFIX, name)
+
+#if !defined(__STDC_LIB_EXT1__)
+static inline int memset_s(void *dest, size_t destsz, int ch, size_t count) {
+ (void)destsz;
+ memset(dest, ch, count);
+ return 0;
+}
+#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,
+ 0x8aULL, 0x88ULL, 0x80008009ULL, 0x8000000aULL,
+ 0x8000808bULL, 0x800000000000008bULL, 0x8000000000008089ULL, 0x8000000000008003ULL,
+ 0x8000000000008002ULL, 0x8000000000000080ULL, 0x800aULL, 0x800000008000000aULL,
+ 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;)
+
+static inline void keccakf(void *state) {
+ uint64_t *a = (uint64_t *)state;
+ uint64_t b[5] = {0};
+ uint64_t t = 0;
+ uint8_t x, y;
+
+ for (int i = 0; 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];
+ }
+}
+
+#define _(S) do { S } while (0)
+#define FOR(i, ST, L, S) \
+ _(for (size_t i = 0; i < L; i += ST) { S; })
+#define mkapply_ds(NAME, S) \
+ static inline void NAME(uint8_t *dst, const uint8_t *src, size_t len) { \
+ FOR(i, 1, len, S); \
+ }
+#define mkapply_sd(NAME, S) \
+ static inline void NAME(const uint8_t *src, uint8_t *dst, size_t len) { \
+ FOR(i, 1, len, S); \
+ }
+
+mkapply_ds(xorin, dst[i] ^= src[i])
+mkapply_sd(setout, dst[i] = src[i])
+
+#define Plen 200
+
+#define foldP(I, L, F) \
+ while (L >= rate) { \
+ F(a, I, rate); \
+ keccakf(a); \
+ I += rate; \
+ L -= rate; \
+ }
+
+static inline int kf_hash_impl(uint8_t *out, size_t outlen,
+ const uint8_t *in, size_t inlen,
+ size_t rate, uint8_t delim) {
+ if ((out == NULL) || ((in == NULL) && inlen != 0) || (rate >= Plen)) {
+ return -1;
+ }
+ uint8_t a[Plen] = {0};
+ foldP(in, inlen, xorin);
+ a[inlen] ^= delim;
+ a[rate - 1] ^= 0x80;
+ xorin(a, in, inlen);
+ keccakf(a);
+ foldP(out, outlen, setout);
+ setout(a, out, outlen);
+ memset_s(a, 200, 0, 200);
+ return 0;
+}
+
+#define defshake(bits) \
+ KF_LINKAGE int KF_FN(shake##bits)(uint8_t *out, size_t outlen, \
+ const uint8_t *in, size_t inlen) { \
+ return kf_hash_impl(out, outlen, in, inlen, 200 - (bits / 4), 0x1f); \
+ }
+#define defsha3(bits) \
+ KF_LINKAGE int KF_FN(sha3_##bits)(uint8_t *out, size_t outlen, \
+ const uint8_t *in, size_t inlen) { \
+ if (outlen > (bits / 8)) { \
+ return -1; \
+ } \
+ return kf_hash_impl(out, outlen, in, inlen, 200 - (bits / 4), 0x06); \
+ }
+
+defshake(128)
+defshake(256)
+defsha3(224)
+defsha3(256)
+defsha3(384)
+defsha3(512)
+
+#undef defshake
+#undef defsha3
+
+#endif /* FINWO_SCALAR_IMPL_H */
diff --git a/src/backend/scalar.c b/src/backend/scalar.c
@@ -11,140 +11,19 @@
#include "../keccak-fast-internal.h"
-/* The fallback must run on any x86-64; clang vectorizes it under -march=native
- * and runs 1.9x slower. */
+/* The fallback must run on any x86-64: no AVX (clang vectorizes it under
+ * -march=native and runs 1.9x slower) and no BMI (that is scalar_bmi.c). */
#if defined(__clang__) && (defined(__x86_64__) || defined(__i386__))
#pragma clang attribute push( \
- __attribute__((target("no-avx,no-avx2,no-avx512f"))), apply_to=function)
+ __attribute__((target("no-avx,no-avx2,no-avx512f,no-bmi,no-bmi2"))), \
+ apply_to=function)
#elif defined(__GNUC__) && (defined(__x86_64__) || defined(__i386__))
-#pragma GCC target("no-avx,no-avx2,no-avx512f")
+#pragma GCC target("no-avx,no-avx2,no-avx512f,no-bmi,no-bmi2")
#endif
-#if !defined(__STDC_LIB_EXT1__)
-static inline int memset_s(void *dest, size_t destsz, int ch, size_t count) {
- (void)destsz;
- memset(dest, ch, count);
- return 0;
-}
-#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,
- 0x8aULL, 0x88ULL, 0x80008009ULL, 0x8000000aULL,
- 0x8000808bULL, 0x800000000000008bULL, 0x8000000000008089ULL, 0x8000000000008003ULL,
- 0x8000000000008002ULL, 0x8000000000000080ULL, 0x800aULL, 0x800000008000000aULL,
- 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;)
-
-static inline void keccakf(void *state) {
- uint64_t *a = (uint64_t *)state;
- uint64_t b[5] = {0};
- uint64_t t = 0;
- uint8_t x, y;
-
- for (int i = 0; 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];
- }
-}
-
-#define _(S) do { S } while (0)
-#define FOR(i, ST, L, S) \
- _(for (size_t i = 0; i < L; i += ST) { S; })
-#define mkapply_ds(NAME, S) \
- static inline void NAME(uint8_t *dst, const uint8_t *src, size_t len) { \
- FOR(i, 1, len, S); \
- }
-#define mkapply_sd(NAME, S) \
- static inline void NAME(const uint8_t *src, uint8_t *dst, size_t len) { \
- FOR(i, 1, len, S); \
- }
-
-mkapply_ds(xorin, dst[i] ^= src[i])
-mkapply_sd(setout, dst[i] = src[i])
-
-#define P keccakf
-#define Plen 200
-
-#define foldP(I, L, F) \
- while (L >= rate) { \
- F(a, I, rate); \
- P(a); \
- I += rate; \
- L -= rate; \
- }
-
-static inline int kf_scalar_hash(uint8_t *out, size_t outlen,
- const uint8_t *in, size_t inlen,
- size_t rate, uint8_t delim) {
- if ((out == NULL) || ((in == NULL) && inlen != 0) || (rate >= Plen)) {
- return -1;
- }
- uint8_t a[Plen] = {0};
- foldP(in, inlen, xorin);
- a[inlen] ^= delim;
- a[rate - 1] ^= 0x80;
- xorin(a, in, inlen);
- P(a);
- foldP(out, outlen, setout);
- setout(a, out, outlen);
- memset_s(a, 200, 0, 200);
- return 0;
-}
-
-#define defshake(bits) \
- int kf_scalar_shake##bits(uint8_t *out, size_t outlen, const uint8_t *in, \
- size_t inlen) { \
- return kf_scalar_hash(out, outlen, in, inlen, 200 - (bits / 4), 0x1f); \
- }
-#define defsha3(bits) \
- int kf_scalar_sha3_##bits(uint8_t *out, size_t outlen, const uint8_t *in, \
- size_t inlen) { \
- if (outlen > (bits / 8)) { \
- return -1; \
- } \
- return kf_scalar_hash(out, outlen, in, inlen, 200 - (bits / 4), 0x06); \
- }
-
-defshake(128)
-defshake(256)
-defsha3(224)
-defsha3(256)
-defsha3(384)
-defsha3(512)
+#define KF_PREFIX kf_scalar_
+#define KF_LINKAGE
+#include "scalar-impl.h"
void kf_scalar_permute(uint64_t state[25]) {
keccakf(state);
diff --git a/src/backend/scalar_bmi.c b/src/backend/scalar_bmi.c
@@ -0,0 +1,61 @@
+/* Runtime BMI1/BMI2 variant of the scalar backend. Same source as scalar.c,
+ * but the target enables `andn` (replaces `not`+`and` in chi) and `rorx`. */
+
+#include <stddef.h>
+#include <stdint.h>
+#include <string.h>
+
+#include "../keccak-fast-internal.h"
+
+#if (defined(__x86_64__) || defined(__i386__)) && !defined(KECCAK_FAST_NO_BMI)
+
+/* clang schedules `rorx` worse than `rol`; gcc is faster with it. */
+#if defined(__clang__)
+#pragma clang attribute push( \
+ __attribute__((target("no-avx,no-avx2,no-avx512f,bmi,no-bmi2"))), \
+ apply_to=function)
+#elif defined(__GNUC__)
+#pragma GCC target("no-avx,no-avx2,no-avx512f,bmi,bmi2")
+#endif
+
+#define KF_PREFIX kf_bmi_
+#define KF_LINKAGE static
+#include "scalar-impl.h"
+
+static void kf_bmi_batch(uint64_t states[][25], size_t count) {
+ for (size_t i = 0; i < count; i++) {
+ keccakf(states[i]);
+ }
+}
+
+__attribute__((constructor)) static void kf_bmi_register(void) {
+#if defined(__clang__)
+ if (!__builtin_cpu_supports("bmi")) {
+#else
+ if (!__builtin_cpu_supports("bmi") || !__builtin_cpu_supports("bmi2")) {
+#endif
+ return;
+ }
+ if (kf_backend_priority <= KF_PRIO_BMI) {
+ kf_shake128 = kf_bmi_shake128;
+ kf_shake256 = kf_bmi_shake256;
+ kf_sha3_224 = kf_bmi_sha3_224;
+ kf_sha3_256 = kf_bmi_sha3_256;
+ kf_sha3_384 = kf_bmi_sha3_384;
+ kf_sha3_512 = kf_bmi_sha3_512;
+ kf_backend_priority = KF_PRIO_BMI;
+ kf_backend_label = "scalar+bmi";
+ }
+ if (kf_batch_priority <= KF_PRIO_BMI) {
+ kf_batch_perm = kf_bmi_batch;
+ kf_batch_priority = KF_PRIO_BMI;
+ kf_batch_lanes = 1;
+ kf_batch_label = "scalar+bmi";
+ }
+}
+
+#if defined(__clang__)
+#pragma clang attribute pop
+#endif
+
+#endif
diff --git a/src/keccak-fast-internal.h b/src/keccak-fast-internal.h
@@ -11,6 +11,7 @@ typedef void (*kf_batch_perm_fn)(uint64_t states[][25], size_t count);
#define KF_LANES_MAX 8
#define KF_PRIO_SCALAR 0
+#define KF_PRIO_BMI 10
#define KF_PRIO_AVX2 20
#define KF_PRIO_AVX512 30