From: Mykyta Yatsenko <mykyta.yatsenko5@gmail.com>
To: "T.J. Mercier" <tjmercier@google.com>,
ast@kernel.org, daniel@iogearbox.net, andrii@kernel.org,
eddyz87@gmail.com, memxor@gmail.com, martin.lau@linux.dev,
song@kernel.org, yonghong.song@linux.dev, jolsa@kernel.org,
emil@etsalapatis.com
Cc: bpf@vger.kernel.org, linux-kernel@vger.kernel.org
Subject: Re: [PATCH bpf-next v3 2/2] bpf: htab: Reduce elem_size by 8 bytes for small key sizes
Date: Fri, 7 Aug 2026 18:33:34 +0100 [thread overview]
Message-ID: <7ce1d67b-ae88-445c-b4f8-ffa0eea9befc@gmail.com> (raw)
In-Reply-To: <20260805223516.1495988-3-tjmercier@google.com>
On 8/5/26 11:35 PM, T.J. Mercier wrote:
> For standard and PCPU (non-LRU) hash maps with small key sizes (less
> than or equal to the word size), comparing keys requires only a single
> instruction. Storing a cached 32-bit hash value to shortcut full key
> comparisons provides no performance advantage for small keys, and
> consumes memory for every element.
>
> This memory can be saved by eliminating hash along with its associated
> 4 byte padding before the key, reducing the elem_size (and key_offset)
> by 8 bytes for standard and PCPU maps.
>
> Introduce htab_has_hash() to check whether a map requires a cached hash
> field. Update htab_elem_set_hash(), lookup_elem_raw(), and
> lookup_nulls_elem_raw() to conditionally bypass hash checking and
> storage when htab_has_hash() is false.
>
> Together with the previous patch, this reduces the minimum standard and
> preallocated hash map element size from 64 bytes down to 32 bytes, and
> non-preallocated per-CPU element size from 64 bytes down to 40 bytes.
>
> Signed-off-by: T.J. Mercier <tjmercier@google.com>
> ---
> kernel/bpf/hashtab.c | 108 +++++++++++++-----
> .../selftests/bpf/progs/map_ptr_kern.c | 2 +-
> 2 files changed, 79 insertions(+), 31 deletions(-)
>
> diff --git a/kernel/bpf/hashtab.c b/kernel/bpf/hashtab.c
> index f54366da459f..9967268d453d 100644
> --- a/kernel/bpf/hashtab.c
> +++ b/kernel/bpf/hashtab.c
> @@ -100,6 +100,7 @@ struct bpf_htab {
> struct percpu_counter pcount;
> atomic_t count;
> bool use_percpu_counter;
> + bool has_hash;
> u32 n_buckets; /* number of hash buckets */
> u32 elem_size; /* size of each element in bytes */
> u32 key_offset; /* offset of key in bytes */
> @@ -123,15 +124,13 @@ struct htab_node {
>
> struct htab_elem {
> struct htab_node node;
> - u32 hash;
> - char key[] __aligned(8);
> + u8 data[] __aligned(8);
> };
>
> struct htab_elem_lru {
> struct htab_node node;
> struct bpf_lru_node lru_node;
> - u32 hash;
> - char key[] __aligned(8);
> + u8 data[] __aligned(8);
> };
>
> /* Only for non-preallocated PCPU maps. Preallocated PCPU maps don't need
> @@ -140,13 +139,13 @@ struct htab_elem_lru {
> struct htab_elem_pcpu {
> struct htab_node node;
> void *ptr_to_pptr;
> - u32 hash;
> - char key[] __aligned(8);
> + u8 data[] __aligned(8);
> };
>
> struct htab_btf_record {
> struct btf_record *record;
> u32 key_size;
> + u32 key_offset;
> };
>
> static inline bool htab_is_prealloc(const struct bpf_htab *htab)
> @@ -247,24 +246,31 @@ static struct htab_elem *get_htab_elem(struct bpf_htab *htab, int i)
> return (struct htab_elem *) (htab->elems + i * (u64)htab->elem_size);
> }
>
> +static inline bool htab_has_hash(const struct bpf_htab *htab)
> +{
> + return htab->has_hash;
> +}
> +
> static inline u32 htab_elem_hash(struct bpf_htab *htab, struct htab_elem *l)
> {
> if (htab_is_lru(htab))
> - return ((struct htab_elem_lru *)l)->hash;
> + return *(u32 *)((struct htab_elem_lru *)l)->data;
> else if (htab_is_percpu(htab) && !htab_is_prealloc(htab))
> - return ((struct htab_elem_pcpu *)l)->hash;
> + return *(u32 *)((struct htab_elem_pcpu *)l)->data;
> else
> - return l->hash;
> + return *(u32 *)l->data;
> }
>
> static inline void htab_elem_set_hash(struct bpf_htab *htab, struct htab_elem *l, u32 hash)
> {
> + if (!htab_has_hash(htab))
> + return;
> if (htab_is_lru(htab))
> - ((struct htab_elem_lru *)l)->hash = hash;
> + *(u32 *)((struct htab_elem_lru *)l)->data = hash;
> else if (htab_is_percpu(htab) && !htab_is_prealloc(htab))
> - ((struct htab_elem_pcpu *)l)->hash = hash;
> + *(u32 *)((struct htab_elem_pcpu *)l)->data = hash;
> else
> - l->hash = hash;
> + *(u32 *)l->data = hash;
> }
>
> /* Both percpu and fd htab support in-place update, so no need for
> @@ -405,7 +411,7 @@ static int prealloc_init(struct bpf_htab *htab)
> if (htab_is_lru(htab))
> err = bpf_lru_init(&htab->lru,
> htab->map.map_flags & BPF_F_NO_COMMON_LRU,
> - offsetof(struct htab_elem_lru, hash) -
> + offsetof(struct htab_elem_lru, data) -
> offsetof(struct htab_elem_lru, lru_node),
> htab_lru_map_delete_node,
> htab);
> @@ -532,7 +538,7 @@ static void htab_mem_dtor(void *obj, void *ctx)
> if (IS_ERR_OR_NULL(hrec->record))
> return;
>
> - map_value = (void *)elem + sizeof(struct htab_elem) + round_up(hrec->key_size, 8);
> + map_value = (void *)elem + hrec->key_offset + round_up(hrec->key_size, 8);
> bpf_obj_free_fields(hrec->record, map_value);
> }
>
> @@ -558,7 +564,7 @@ static void htab_dtor_ctx_free(void *ctx)
> }
>
> static int bpf_ma_set_dtor(struct bpf_map *map, struct bpf_mem_alloc *ma,
> - void (*dtor)(void *, void *))
> + void (*dtor)(void *, void *), u32 key_offset)
do we need to add key_offset param? What if we read key_offset from map->htab->key_offset?
> {
> struct htab_btf_record *hrec;
> int err;
> @@ -571,6 +577,7 @@ static int bpf_ma_set_dtor(struct bpf_map *map, struct bpf_mem_alloc *ma,
> if (!hrec)
> return -ENOMEM;
> hrec->key_size = map->key_size;
> + hrec->key_offset = key_offset;
> hrec->record = btf_record_dup(map->record);
> if (IS_ERR(hrec->record)) {
> err = PTR_ERR(hrec->record);
> @@ -593,9 +600,9 @@ static int htab_map_check_btf(struct bpf_map *map, const struct btf *btf,
> * populated in htab_map_alloc(), so it will always appear as NULL.
> */
> if (htab_is_percpu(htab))
> - return bpf_ma_set_dtor(map, &htab->pcpu_ma, htab_pcpu_mem_dtor);
> + return bpf_ma_set_dtor(map, &htab->pcpu_ma, htab_pcpu_mem_dtor, htab->key_offset);
> else
> - return bpf_ma_set_dtor(map, &htab->ma, htab_mem_dtor);
> + return bpf_ma_set_dtor(map, &htab->ma, htab_mem_dtor, htab->key_offset);
> }
>
> static struct bpf_map *htab_map_alloc(union bpf_attr *attr)
> @@ -618,6 +625,13 @@ static struct bpf_map *htab_map_alloc(union bpf_attr *attr)
>
> bpf_map_init_from_attr(&htab->map, attr);
>
> + /* Avoid hash memory use and comparisons where unnecessary.
> + * u32 hash reads are always atomic. If we elide them, key comparisons must also be atomic
> + * to avoid false positive key matches due to torn key reads / writes. This is only possible
> + * when the key fits within a word, so check key_size.
> + */
> + htab->has_hash = htab_is_lru(htab) || htab->map.key_size > sizeof(unsigned long);
> +
> if (percpu_lru) {
> /* ensure each CPU's lru list has >=1 elements.
> * since we are at it, make each lru list has the same
> @@ -640,11 +654,13 @@ static struct bpf_map *htab_map_alloc(union bpf_attr *attr)
> htab->n_buckets = roundup_pow_of_two(htab->map.max_entries);
>
> if (htab_is_lru(htab))
> - htab->key_offset = sizeof(struct htab_elem_lru);
> + htab->key_offset = offsetof(struct htab_elem_lru, data) + 8;
> else if (percpu && !prealloc)
> - htab->key_offset = sizeof(struct htab_elem_pcpu);
> + htab->key_offset = offsetof(struct htab_elem_pcpu, data) +
> + (htab_has_hash(htab) ? 8 : 0);
> else
> - htab->key_offset = sizeof(struct htab_elem);
> + htab->key_offset = offsetof(struct htab_elem, data) +
> + (htab_has_hash(htab) ? 8 : 0);
>
> htab->elem_size = htab->key_offset + round_up(htab->map.key_size, 8);
> if (percpu)
> @@ -761,10 +777,22 @@ static struct htab_elem *lookup_elem_raw(struct bpf_htab *htab,
> struct hlist_nulls_node *n;
> struct htab_elem *l;
>
> - hlist_nulls_for_each_entry_rcu(l, n, head, node.hash_node)
> - if (htab_elem_hash(htab, l) == hash &&
> - !memcmp(htab_elem_key(htab, l), key, key_size))
> - return l;
> + if (htab_has_hash(htab)) {
> + hlist_nulls_for_each_entry_rcu(l, n, head, node.hash_node)
> + if (htab_elem_hash(htab, l) == hash &&
> + !memcmp(htab_elem_key(htab, l), key, key_size))
> + return l;
> + } else {
> + /* When hash is omitted, key comparisons must be atomic. Zero extend
> + * the caller's key to the word size to support an atomic compare.
> + */
> + unsigned long k = 0;
> +
> + memcpy(&k, key, key_size);
> + hlist_nulls_for_each_entry_rcu(l, n, head, node.hash_node)
> + if (READ_ONCE(*(unsigned long *)htab_elem_key(htab, l)) == k)
> + return l;
> + }
>
> return NULL;
> }
> @@ -782,10 +810,20 @@ static struct htab_elem *lookup_nulls_elem_raw(struct bpf_htab *htab,
> struct htab_elem *l;
>
> again:
> - hlist_nulls_for_each_entry_rcu(l, n, head, node.hash_node)
> - if (htab_elem_hash(htab, l) == hash &&
> - !memcmp(htab_elem_key(htab, l), key, key_size))
> - return l;
> + if (htab_has_hash(htab)) {
> + hlist_nulls_for_each_entry_rcu(l, n, head, node.hash_node)
> + if (htab_elem_hash(htab, l) == hash &&
> + !memcmp(htab_elem_key(htab, l), key, key_size))
> + return l;
> + } else {
> + /* See lookup_elem_raw() comment above. */
> + unsigned long k = 0;
> +
> + memcpy(&k, key, key_size);
> + hlist_nulls_for_each_entry_rcu(l, n, head, node.hash_node)
> + if (READ_ONCE(*(unsigned long *)htab_elem_key(htab, l)) == k)
> + return l;
> + }
This hunk is quite big, maybe move it to a separate function to reuse in
lookup_elem_raw() and lookup_nulls_elem_raw().
>
> if (unlikely(get_nulls_value(n) != (hash & (n_buckets - 1))))
> goto again;
> @@ -1188,7 +1226,17 @@ static struct htab_elem *alloc_htab_elem(struct bpf_htab *htab, void *key,
> }
> }
>
> - memcpy(htab_elem_key(htab, l_new), key, key_size);
> + if (htab_has_hash(htab)) {
> + memcpy(htab_elem_key(htab, l_new), key, key_size);
> + } else {
> + /* Zero-extend key into k for an atomic write to support
> + * lockless RCU readers.
> + */
> + unsigned long k = 0;
> +
> + memcpy(&k, key, key_size);
> + WRITE_ONCE(*(unsigned long *)htab_elem_key(htab, l_new), k);
> + }
> if (percpu) {
> if (prealloc) {
> pptr = htab_elem_get_ptr(htab, l_new);
> @@ -3191,7 +3239,7 @@ static int rhtab_map_check_btf(struct bpf_map *map, const struct btf *btf,
> {
> struct bpf_rhtab *rhtab = container_of(map, struct bpf_rhtab, map);
>
> - return bpf_ma_set_dtor(map, &rhtab->ma, rhtab_mem_dtor);
> + return bpf_ma_set_dtor(map, &rhtab->ma, rhtab_mem_dtor, offsetof(struct rhtab_elem, data));
> }
>
> static void rhtab_map_free_internal_structs(struct bpf_map *map)
> diff --git a/tools/testing/selftests/bpf/progs/map_ptr_kern.c b/tools/testing/selftests/bpf/progs/map_ptr_kern.c
> index 373c8d17ea55..6bd4cb68c20c 100644
> --- a/tools/testing/selftests/bpf/progs/map_ptr_kern.c
> +++ b/tools/testing/selftests/bpf/progs/map_ptr_kern.c
> @@ -114,7 +114,7 @@ static inline int check_hash(void)
> VERIFY(check_default_noinline(&hash->map, map));
>
> VERIFY(hash->n_buckets == MAX_ENTRIES);
> - VERIFY(hash->elem_size == 64);
> + VERIFY(hash->elem_size == 32);
>
> VERIFY(hash->count.counter == 0);
> VERIFY(bpf_map_sum_elem_count(map) == 0);
prev parent reply other threads:[~2026-08-07 17:33 UTC|newest]
Thread overview: 6+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-08-05 22:35 [PATCH bpf-next v3 0/2] bpf: htab: Reduce memory use of hash maps T.J. Mercier
2026-08-05 22:35 ` [PATCH bpf-next v3 1/2] bpf: htab: Split htab_elem_lru and htab_elem_pcpu off of htab_elem T.J. Mercier
2026-08-07 15:25 ` Mykyta Yatsenko
2026-08-05 22:35 ` [PATCH bpf-next v3 2/2] bpf: htab: Reduce elem_size by 8 bytes for small key sizes T.J. Mercier
2026-08-05 22:48 ` sashiko-bot
2026-08-07 17:33 ` Mykyta Yatsenko [this message]
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=7ce1d67b-ae88-445c-b4f8-ffa0eea9befc@gmail.com \
--to=mykyta.yatsenko5@gmail.com \
--cc=andrii@kernel.org \
--cc=ast@kernel.org \
--cc=bpf@vger.kernel.org \
--cc=daniel@iogearbox.net \
--cc=eddyz87@gmail.com \
--cc=emil@etsalapatis.com \
--cc=jolsa@kernel.org \
--cc=linux-kernel@vger.kernel.org \
--cc=martin.lau@linux.dev \
--cc=memxor@gmail.com \
--cc=song@kernel.org \
--cc=tjmercier@google.com \
--cc=yonghong.song@linux.dev \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox