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 v4 8/9] selftests/bpf: libarena: Add a transfer cache
Date: Mon, 9 Nov 2026 18:20:53 +0000 [thread overview]
Message-ID: <20261109182055.7581-9-emil@etsalapatis.com> (raw)
In-Reply-To: <20261109182055.7581-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. Use
trylock for both locks in NMI context, deferring frees when the buddy lock
cannot be acquired. Keep both slow paths out of line so that cache hits
do not pay for them.
October 21 measurements in a 16-vCPU VM (means of three runs, 64 or
256 allocations per cycle, 2-8 producers) showed 17-60% lower allocation
latency and 60-74% lower free latency than direct bulk spilling. With
8 producers and 256 allocations per cycle, cross-CPU allocation/free
latencies dropped from 1188/1218 ns to 482/408 ns.
Signed-off-by: Emil Tsalapatis <emil@etsalapatis.com>
---
.../bpf/libarena/include/libarena/buddy.h | 11 ++
.../selftests/bpf/libarena/src/buddy.bpf.c | 122 +++++++++++++++---
2 files changed, 118 insertions(+), 15 deletions(-)
diff --git a/tools/testing/selftests/bpf/libarena/include/libarena/buddy.h b/tools/testing/selftests/bpf/libarena/include/libarena/buddy.h
index a4d96909f5b0..154a5d1ed0f1 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 a23da811e484..ef7e87344e83 100644
--- a/tools/testing/selftests/bpf/libarena/src/buddy.bpf.c
+++ b/tools/testing/selftests/bpf/libarena/src/buddy.bpf.c
@@ -128,9 +128,81 @@ 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;
+
+ ret = buddy_lock(transfer, flags);
+ if (ret)
+ 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;
+
+ ret = buddy_lock(transfer, flags);
+
+ if (ret)
+ 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,
@@ -138,6 +210,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_nogrow(buddy, size, BUDDY_CACHE_BATCH_ORDER,
(void __arena * __arena *)slots->objects))
return NULL;
@@ -158,9 +235,8 @@ void __arena *buddy_cache_alloc(struct buddy __arena *buddy, size_t size, size_t
if (!cache)
return NULL;
- /* NMIs cannot take the buddy lock, so they only use what is already cached. */
address = buddy_cache_pop(cache, order);
- if (!address && !bpf_in_nmi())
+ if (!address)
address = buddy_cache_refill(buddy, cache, size, order);
buddy_cache_exit(cache);
@@ -194,9 +270,10 @@ static int buddy_cache_get_order(void __arena *addr)
}
/*
- * Make room in a full cache by freeing its top batch to the buddy allocator,
- * then cache the freed object. Kept out of line so that frees that fit in the
- * cache do not pay for it.
+ * 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,
@@ -204,15 +281,25 @@ buddy_cache_spill(struct buddy __arena *buddy, struct buddy_cache __arena *cache
{
struct buddy_cache_order __arena *slots = &cache->orders[order];
u32 elems;
- int ret;
+ int ret = -ENOMEM;
- 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;
+ /*
+ * Peek without the lock, buddy_transfer_put_batch() rechecks under it.
+ * NMIs just try the transfer lock and return immediately on failure.
+ */
+ if (READ_ONCE(buddy->transfer[order].elems) <=
+ BUDDY_TRANSFER_CAPACITY - BUDDY_CACHE_BATCH_SIZE)
+ ret = buddy_transfer_put_batch(buddy, order, slots);
- WRITE_ONCE(slots->elems, elems);
+ 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);
}
@@ -769,6 +856,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]);
@@ -786,10 +876,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-11-09 18:21 UTC|newest]
Thread overview: 17+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-11-09 18:20 [PATCH bpf-next v4 0/9] bpf libarena allocator improvements Emil Tsalapatis
2026-11-09 18:20 ` [PATCH bpf-next v4 1/9] selftests/bpf: libarena: Add bulk buddy allocation helpers Emil Tsalapatis
2026-11-09 18:20 ` [PATCH bpf-next v4 2/9] selftests/bpf: Add a parallel buddy allocator benchmark Emil Tsalapatis
2026-11-09 18:20 ` [PATCH bpf-next v4 3/9] selftests/bpf: libarena: Make spinlock trylock match arena_spin_unlock Emil Tsalapatis
2026-11-09 18:29 ` sashiko-bot
2026-11-09 18:20 ` [PATCH bpf-next v4 4/9] selftests/bpf: libarena: Raise arena spinlock CPU limits Emil Tsalapatis
2026-11-09 18:32 ` sashiko-bot
2026-11-09 18:20 ` [PATCH bpf-next v4 5/9] selftests/bpf: libarena: Add buddy per-CPU caches Emil Tsalapatis
2026-11-09 18:38 ` sashiko-bot
2026-11-09 18:20 ` [PATCH bpf-next v4 6/9] selftests/bpf: libarena: Handle NMI reentrancy Emil Tsalapatis
2026-11-09 18:42 ` sashiko-bot
2026-11-09 19:05 ` bot+bpf-ci
2026-11-09 19:08 ` Emil Tsalapatis
2026-11-09 18:20 ` [PATCH bpf-next v4 7/9] selftests/bpf: libarena: Refill local caches in batches Emil Tsalapatis
2026-11-09 18:20 ` Emil Tsalapatis [this message]
2026-11-09 18:20 ` [PATCH bpf-next v4 9/9] selftests/bpf: libarena: Cover buddy cache fast paths Emil Tsalapatis
2026-11-09 19:05 ` bot+bpf-ci
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=20261109182055.7581-9-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.