Skip to content

Commit 9247725

Browse files
committed
bpf: use FNV-1a variant for ratelimit hash
FNV-1a is a very simple non-cryptographic hash algorithm. It accumulates each byte of input using an XOR and a multiply. As a result, its code size is ~10x smaller than comparable hash functions. However byte-by-byte hash functions often struggle over large input. Here, FNV-1a is nearly 10x slower than a comparable algorithm, Murmur Hash, for 1K inputs (about the upper bound of what we'd care about here). Word-by-word hash functions like Murmur Hash can be much faster but risk insufficient "mixing" across bits within each word. To provide this bit-mixing, Murmur Hash defines a final mixer ("fmix"). A relatively common way of repurposing a byte-by-byte hasher to word size is to add a mixer and doing so here with "fmix" on top of FNV-1a has great results with no loss of collision-resistence, randomness, or skewedness. Code size: FNV-1a 8mix - 100 ins FNV-1a - 35 ins MurmurHash3 - 350 ins Jenkins - 400 ins 1K Hash Speed: FNV-1a 8mix - 150 ns FNV-1a - 1500 ns MurmurHash3 - 175 ns Jenkins - 2500 ns forge-parent: tkvkksyznwtp
1 parent b29ddec commit 9247725

File tree

2 files changed

+40
-9
lines changed

2 files changed

+40
-9
lines changed

bpf/process/ratelimit_maps.h

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@
1414
#define ACTION_RATE_LIMIT_SCOPE_GLOBAL 2
1515

1616
/* FNV-1a hash constants for 64-bit. */
17-
#define FNV1A_64_INIT ((__u64)0xcbf29ce484222325ULL)
18-
#define FNV1A_64_PRIME ((__u64)0x100000001b3ULL)
17+
#define FNV_OFFSET_BASIS 0xcbf29ce484222325ULL
18+
#define FNV_PRIME 0x100000001b3ULL
1919

2020
/*
2121
* Maximum bytes of each argument to hash for the rate-limit dedup key.
@@ -27,28 +27,59 @@
2727
*/
2828
#define MAX_HASH_BYTES 255
2929

30+
/* MurmurHash3 fmix64 finalizer — guarantees full avalanche. */
31+
static inline __attribute__((always_inline)) __u64
32+
fnv1a_wordmix_fmix64(__u64 h)
33+
{
34+
h ^= h >> 33;
35+
h *= 0xff51afd7ed558ccdULL;
36+
h ^= h >> 33;
37+
h *= 0xc4ceb9fe1a85ec53ULL;
38+
h ^= h >> 33;
39+
return h;
40+
}
41+
3042
/*
3143
* FNV-1a hash of up to MAX_HASH_BYTES of src into a single u64.
44+
* Processes 8-byte words for efficiency, with fmix64 avalanche finalizer.
3245
*/
3346
static inline __attribute__((always_inline)) __u64
34-
fnv1a_hash_bytes(char *src, __u32 len)
47+
fnv1a_wordmix_hash_bytes(char *src, __u32 len)
3548
{
36-
__u64 hash = FNV1A_64_INIT;
37-
__u32 i;
49+
__u64 hash = FNV_OFFSET_BASIS;
50+
__u32 i = 0;
51+
__u64 word;
3852

3953
if (len > MAX_HASH_BYTES)
4054
len = MAX_HASH_BYTES;
41-
/* Mask len so the verifier can prove the loop bound. */
55+
// Mask len so the verifier can prove the loop bound.
4256
asm volatile("%[len] &= 0xff;\n"
4357
: [len] "+r"(len)
4458
:);
4559

46-
for (i = 0; i < MAX_HASH_BYTES; i++) {
60+
// Process 8-bytes per iteration vs 1-byte per iteration in FNV-1a.
61+
for (__u32 j = 0; j < MAX_HASH_BYTES / 8; j++) {
62+
if (i + 8 > len)
63+
break;
64+
__builtin_memcpy(&word, &src[i], sizeof(word));
65+
hash ^= word;
66+
hash *= FNV_PRIME;
67+
i += 8;
68+
}
69+
70+
// Process remaining bytes (at most 7).
71+
for (__u32 j = 0; j < 7; j++) {
4772
if (i >= len)
4873
break;
4974
hash ^= (__u64)(__u8)src[i];
50-
hash *= FNV1A_64_PRIME;
75+
hash *= FNV_PRIME;
76+
i++;
5177
}
78+
79+
// Apply MurmurHash finalizer to ensure bit mixing.
80+
hash ^= (__u64)len;
81+
hash = fnv1a_wordmix_fmix64(hash);
82+
5283
return hash;
5384
}
5485

bpf/process/types/basic.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2404,7 +2404,7 @@ rate_limit(__u64 ratelimit_interval, __u64 ratelimit_scope, struct msg_generic_k
24042404
: [arg_size] "+r"(arg_size)
24052405
:);
24062406
probe_read(scratch, arg_size, &e->args[key_index]);
2407-
key->arg_hash[i] = fnv1a_hash_bytes(scratch, arg_size);
2407+
key->arg_hash[i] = fnv1a_wordmix_hash_bytes(scratch, arg_size);
24082408
}
24092409
}
24102410

0 commit comments

Comments
 (0)