From: Emil Tsalapatis <emil@etsalapatis.com>
To: bpf@vger.kernel.org
Cc: ast@kernel.org, andrii@kernel.org, eddyz87@gmail.com,
memxor@gmail.com, daniel@iogearbox.net,
Emil Tsalapatis <emil@etsalapatis.com>
Subject: [PATCH bpf-next v2 6/7] selftests/bpf: libarena: Add a transfer cache
Date: Wed, 21 Oct 2026 23:26:40 +0000 [thread overview]
Message-ID: <20261021232641.10063-7-emil@etsalapatis.com> (raw)
In-Reply-To: <20261021232641.10063-1-emil@etsalapatis.com>
Batch refills amortize local-cache misses, but every batch that refills
an empty local array, and every free into a full one, still goes through
the buddy lock. Under contention the buddy lock and its metadata updates
dominate, and spare objects on one CPU cannot serve another.
Add a fixed-size shared transfer array for each cached order, protected
by a per-order lock. A full local cache moves one batch into the
transfer array, and an empty one takes a batch from it before falling
back to the buddy allocator. Embed the transfer arrays in struct buddy so
this layer requires no dynamically allocated containers.
Check the transfer array's count before taking its lock, so that a full
or empty array does not cost a lock acquisition that cannot succeed.
When the array is full or its lock is busy, spill the local batch to the
buddy allocator with buddy_free_bulk(), so the next frees fit in the
local cache instead of each taking the transfer and buddy locks. NMI
context never takes the transfer lock and spills directly. Keep both
slow paths out of line so that cache hits do not pay for them.
Signed-off-by: Emil Tsalapatis <emil@etsalapatis.com>
---
.../bpf/libarena/include/libarena/buddy.h | 11 ++
.../selftests/bpf/libarena/src/buddy.bpf.c | 123 +++++++++++++++++-
2 files changed, 131 insertions(+), 3 deletions(-)
diff --git a/tools/testing/selftests/bpf/libarena/include/libarena/buddy.h b/tools/testing/selftests/bpf/libarena/include/libarena/buddy.h
index 0c984952042a..1c62ae6e3fd0 100644
--- a/tools/testing/selftests/bpf/libarena/include/libarena/buddy.h
+++ b/tools/testing/selftests/bpf/libarena/include/libarena/buddy.h
@@ -89,6 +89,9 @@ enum buddy_cache_consts {
* Objects retained locally for each allocation order.
*/
BUDDY_CACHE_CAPACITY = BUDDY_CACHE_BATCH_SIZE * 2,
+
+ /* Number of objects shared between per-CPU caches for each order. */
+ BUDDY_TRANSFER_CAPACITY = BUDDY_CACHE_BATCH_SIZE * 8,
};
struct buddy_cache_order {
@@ -104,11 +107,19 @@ struct buddy_cache {
struct buddy_cache_order orders[BUDDY_CACHE_NUM_ORDERS];
};
+/* Per-order array shared between the per-CPU caches. */
+struct buddy_transfer_order {
+ arena_spinlock_t lock;
+ u32 elems;
+ void __arena *objects[BUDDY_TRANSFER_CAPACITY];
+} __attribute__((aligned(64)));
+
struct buddy {
struct buddy_chunk __arena *first_chunk; /* Pointer to the chunk linked list. */
arena_spinlock_t lock; /* Allocator lock */
u64 vaddr; /* Allocation into reserved vaddr */
struct buddy_cache __arena *percpu[BUDDY_CACHE_MAX_CPUS];
+ struct buddy_transfer_order transfer[BUDDY_CACHE_NUM_ORDERS];
struct buddy_deferred_free __arena *deferred_free;
};
diff --git a/tools/testing/selftests/bpf/libarena/src/buddy.bpf.c b/tools/testing/selftests/bpf/libarena/src/buddy.bpf.c
index d7c3defeb0a8..80f2a49f95e5 100644
--- a/tools/testing/selftests/bpf/libarena/src/buddy.bpf.c
+++ b/tools/testing/selftests/bpf/libarena/src/buddy.bpf.c
@@ -126,9 +126,78 @@ static __always_inline int buddy_cache_push(struct buddy_cache __arena *cache,
return 0;
}
+static int buddy_transfer_take_batch(struct buddy __arena *buddy, int order,
+ struct buddy_cache_order __arena *slots)
+{
+ struct buddy_transfer_order __arena *transfer = &buddy->transfer[order];
+ unsigned long flags;
+ u32 elems, start;
+ unsigned int i;
+ int ret = 0;
+
+ if (arena_spin_lock_irqsave(&transfer->lock, flags))
+ return -EAGAIN;
+
+ elems = READ_ONCE(transfer->elems);
+ if (elems < BUDDY_CACHE_BATCH_SIZE) {
+ ret = -ENOENT;
+ goto out_unlock;
+ }
+
+ start = elems - BUDDY_CACHE_BATCH_SIZE;
+ for (i = zero; i < BUDDY_CACHE_BATCH_SIZE && can_loop; i++)
+ WRITE_ONCE(slots->objects[i],
+ READ_ONCE(transfer->objects[start + i]));
+
+ WRITE_ONCE(transfer->elems, start);
+ WRITE_ONCE(slots->elems, BUDDY_CACHE_BATCH_SIZE);
+
+out_unlock:
+ arena_spin_unlock_irqrestore(&transfer->lock, flags);
+ return ret;
+}
+
+static int buddy_transfer_put_batch(struct buddy __arena *buddy, int order,
+ struct buddy_cache_order __arena *slots)
+{
+ struct buddy_transfer_order __arena *transfer = &buddy->transfer[order];
+ unsigned long flags;
+ u32 elems, local_elems, start;
+ unsigned int i;
+ int ret = 0;
+
+ if (arena_spin_lock_irqsave(&transfer->lock, flags))
+ return -EAGAIN;
+
+ elems = READ_ONCE(transfer->elems);
+ if (elems > BUDDY_TRANSFER_CAPACITY - BUDDY_CACHE_BATCH_SIZE) {
+ ret = -ENOMEM;
+ goto out_unlock;
+ }
+
+ local_elems = READ_ONCE(slots->elems);
+ if (local_elems < BUDDY_CACHE_BATCH_SIZE) {
+ ret = -EINVAL;
+ goto out_unlock;
+ }
+
+ start = local_elems - BUDDY_CACHE_BATCH_SIZE;
+ for (i = zero; i < BUDDY_CACHE_BATCH_SIZE && can_loop; i++)
+ WRITE_ONCE(transfer->objects[elems + i],
+ READ_ONCE(slots->objects[start + i]));
+
+ WRITE_ONCE(slots->elems, start);
+ WRITE_ONCE(transfer->elems, elems + BUDDY_CACHE_BATCH_SIZE);
+
+out_unlock:
+ arena_spin_unlock_irqrestore(&transfer->lock, flags);
+ return ret;
+}
+
/*
- * Refill an empty cache with one trip to the buddy allocator. Kept out of line
- * so that cache hits do not pay for the refill path.
+ * Refill an empty cache from the shared transfer array, or with one trip to the
+ * buddy allocator. Kept out of line so that cache hits do not pay for the refill
+ * path.
*/
static __noinline void __arena *
buddy_cache_refill(struct buddy __arena *buddy, struct buddy_cache __arena *cache,
@@ -136,6 +205,11 @@ buddy_cache_refill(struct buddy __arena *buddy, struct buddy_cache __arena *cach
{
struct buddy_cache_order __arena *slots = &cache->orders[order];
+ /* Peek without the lock, buddy_transfer_take_batch() rechecks under it. */
+ if (READ_ONCE(buddy->transfer[order].elems) >= BUDDY_CACHE_BATCH_SIZE &&
+ !buddy_transfer_take_batch(buddy, order, slots))
+ return buddy_cache_pop(cache, order);
+
if (buddy_alloc_bulk(buddy, size, BUDDY_CACHE_BATCH_ORDER,
(void __arena * __arena *)slots->objects))
return NULL;
@@ -191,6 +265,42 @@ static int buddy_cache_get_order(void __arena *addr)
return idx_get_order(chunk, idx);
}
+/*
+ * Make room in a full cache by moving its top batch to the shared transfer
+ * array, or to the buddy allocator when the array is full or busy, then cache
+ * the freed object. Kept out of line so that frees that fit in the cache do not
+ * pay for it.
+ */
+static __noinline int
+buddy_cache_spill(struct buddy __arena *buddy, struct buddy_cache __arena *cache,
+ int order, void __arena *addr)
+{
+ struct buddy_cache_order __arena *slots = &cache->orders[order];
+ u32 elems;
+ int ret = -ENOMEM;
+
+ /*
+ * Peek without the lock, buddy_transfer_put_batch() rechecks under it.
+ * NMIs never take the transfer lock.
+ */
+ if (!bpf_in_nmi() &&
+ READ_ONCE(buddy->transfer[order].elems) <=
+ BUDDY_TRANSFER_CAPACITY - BUDDY_CACHE_BATCH_SIZE)
+ ret = buddy_transfer_put_batch(buddy, order, slots);
+
+ if (ret) {
+ elems = READ_ONCE(slots->elems) - BUDDY_CACHE_BATCH_SIZE;
+ ret = buddy_free_bulk(buddy, (void __arena * __arena *)&slots->objects[elems],
+ BUDDY_CACHE_BATCH_SIZE);
+ if (ret)
+ return ret;
+
+ WRITE_ONCE(slots->elems, elems);
+ }
+
+ return buddy_cache_push(cache, order, addr);
+}
+
int buddy_cache_free(struct buddy __arena *buddy, void __arena *addr)
{
struct buddy_cache __arena *cache;
@@ -210,6 +320,8 @@ int buddy_cache_free(struct buddy __arena *buddy, void __arena *addr)
return -EAGAIN;
ret = buddy_cache_push(cache, order, addr);
+ if (ret)
+ ret = buddy_cache_spill(buddy, cache, order, addr);
buddy_cache_exit(cache);
return ret;
@@ -741,6 +853,9 @@ static int buddy_cache_init(struct buddy __arena *buddy)
if (CONFIG_NR_CPUS > BUDDY_CACHE_MAX_CPUS)
return -E2BIG;
+ for (order = zero; order < BUDDY_CACHE_NUM_ORDERS && can_loop; order++)
+ WRITE_ONCE(buddy->transfer[order].elems, 0);
+
for (cpu = zero; cpu < CONFIG_NR_CPUS && can_loop; cpu++) {
ret = buddy_alloc_bulk(buddy, sizeof(*cache), 0,
(void __arena * __arena *)&buddy->percpu[cpu]);
@@ -758,10 +873,12 @@ static int buddy_cache_init(struct buddy __arena *buddy)
static void buddy_cache_destroy(struct buddy __arena *buddy)
{
- unsigned int cpu;
+ unsigned int cpu, order;
for (cpu = zero; cpu < min(CONFIG_NR_CPUS, BUDDY_CACHE_MAX_CPUS) && can_loop; cpu++)
WRITE_ONCE(buddy->percpu[cpu], NULL);
+ for (order = zero; order < BUDDY_CACHE_NUM_ORDERS && can_loop; order++)
+ WRITE_ONCE(buddy->transfer[order].elems, 0);
}
__weak int buddy_init(struct buddy __arena *buddy)
--
2.52.0
next prev parent reply other threads:[~2026-10-21 23:27 UTC|newest]
Thread overview: 11+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-21 23:26 [PATCH bpf-next v2 0/7] bpf libarena allocator improvements Emil Tsalapatis
2026-10-21 23:26 ` [PATCH bpf-next v2 1/7] selftests/bpf: libarena: Add bulk buddy allocation helpers Emil Tsalapatis
2026-10-21 23:26 ` [PATCH bpf-next v2 2/7] selftests/bpf: Add a parallel buddy allocator benchmark Emil Tsalapatis
2026-10-21 23:26 ` [PATCH bpf-next v2 3/7] selftests/bpf: libarena: Add buddy per-CPU caches Emil Tsalapatis
2026-10-22 12:53 ` Harry Yoo
2026-10-22 23:40 ` Emil Tsalapatis
2026-10-22 23:52 ` Kumar Kartikeya Dwivedi
2026-10-21 23:26 ` [PATCH bpf-next v2 4/7] selftests/bpf: libarena: Handle NMI reentrancy Emil Tsalapatis
2026-10-21 23:26 ` [PATCH bpf-next v2 5/7] selftests/bpf: libarena: Refill local caches in batches Emil Tsalapatis
2026-10-21 23:26 ` Emil Tsalapatis [this message]
2026-10-21 23:26 ` [PATCH bpf-next v2 7/7] selftests/bpf: libarena: Cover buddy cache fast paths Emil Tsalapatis
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=20261021232641.10063-7-emil@etsalapatis.com \
--to=emil@etsalapatis.com \
--cc=andrii@kernel.org \
--cc=ast@kernel.org \
--cc=bpf@vger.kernel.org \
--cc=daniel@iogearbox.net \
--cc=eddyz87@gmail.com \
--cc=memxor@gmail.com \
/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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.