* [PATCH bpf-next v5 0/9] bpf libarena allocator improvements
@ 2026-11-10 0:38 Emil Tsalapatis
2026-11-10 0:38 ` [PATCH bpf-next v5 1/9] selftests/bpf: libarena: Add bulk buddy allocation helpers Emil Tsalapatis
` (9 more replies)
0 siblings, 10 replies; 15+ messages in thread
From: Emil Tsalapatis @ 2026-11-10 0:38 UTC (permalink / raw)
To: bpf; +Cc: ast, andrii, eddyz87, memxor, daniel, Emil Tsalapatis, Harry Yoo
The libarena buddy allocator's design has up until now prioritized
ease of verification. With new verifier features like unlimited
global stack frames and a large stack size we can now improve
the allocator's performance and robustness.
This patch introduces per-CPU caches for the allocator. The overall
caching design is inspired by tcmalloc. Each CPU has its own static
object LIFOs, one for each allocator order. This speeds up allocation
and freeing for BPF programs that have some degree of locality. The
CPU caches are lockless. The caches are replenished from and spilled
into "transfer caches", per-order caches shared between the CPUs that
amortize the cost of spilling/refilling. Data from the transfer caches
is in turn moved from and to the allocator.
The patchset additionally includes code for allocator reentrancy and
bulk allocations. The reentrancy code lets us safely allocate from an
NMI context in general, and from a BPF syscall program context
locklessly. The bulk allocation API is useful for replenishing the
allocator caches and also on its own right for speeding up allocations
during initialization.
Benchmark results from patch 2, run with the options from
run_bench_libarena.sh (-a, 64-byte allocations) in a 16-vCPU VM, as the
mean of three runs. Rows are the number of consecutive allocations, and
then frees, in each benchmark iteration. Columns are the number of
producers. Each entry shows the latency in ns before and with this
series, followed by the speedup: the latency before divided by the
latency with this series, so higher is better.
libarena-malloc-free, where each worker frees its own objects:
Allocation latency (before/after ns, speedup):
allocs 1 prod 2 prod 4 prod 8 prod
1 269/68 3.9x 641/68 9.4x 1568/68 22.9x 4550/69 66.4x
4 251/63 4.0x 557/63 8.9x 2326/67 34.5x 5221/74 70.4x
16 247/61 4.0x 1027/64 16.0x 2570/67 38.1x 5315/74 71.8x
64 242/75 3.2x 1129/146 7.8x 2469/190 13.0x 5521/374 14.8x
256 242/102 2.4x 1314/184 7.1x 2664/249 10.7x 5542/457 12.1x
Free latency (before/after ns, speedup):
allocs 1 prod 2 prod 4 prod 8 prod
1 243/55 4.4x 273/56 4.9x 1428/55 25.9x 4371/56 78.2x
4 242/51 4.7x 414/52 8.0x 2303/52 44.5x 5266/51 103.1x
16 241/50 4.8x 1056/50 20.9x 2580/50 51.6x 5330/51 105.4x
64 240/64 3.8x 1079/86 12.5x 2454/131 18.8x 5538/309 17.9x
256 238/124 1.9x 1206/107 11.2x 2681/178 15.0x 5546/406 13.7x
libarena-malloc-xfree, where each worker frees the objects allocated by
the previous invocation, so with more than one producer most frees
happen on a different CPU from the allocation:
Allocation latency (before/after ns, speedup):
allocs 1 prod 2 prod 4 prod 8 prod
1 291/65 4.5x 857/65 13.2x 1165/67 17.5x 4172/70 59.8x
4 243/63 3.9x 754/64 11.7x 2170/66 32.6x 4931/79 62.2x
16 231/61 3.8x 1163/63 18.5x 2572/71 36.2x 5438/93 58.7x
64 229/75 3.1x 1249/158 7.9x 2824/213 13.3x 5760/411 14.0x
256 228/100 2.3x 1269/210 6.0x 2746/264 10.4x 5669/466 12.2x
Free latency (before/after ns, speedup):
allocs 1 prod 2 prod 4 prod 8 prod
1 266/55 4.8x 558/56 10.0x 1052/56 18.8x 4050/56 72.3x
4 231/51 4.5x 583/52 11.2x 2032/51 39.6x 4875/52 93.9x
16 223/50 4.5x 1019/50 20.3x 2492/50 49.6x 5396/51 106.5x
64 222/63 3.5x 1156/84 13.8x 2801/122 23.0x 5735/320 17.9x
256 221/124 1.8x 1202/113 10.7x 2736/160 17.1x 5656/387 14.6x
Cc: Harry Yoo <harry@kernel.org>
Cc: Kumar Kartikeya Dwivedi <memxor@gmail.com>
Signed-off-by: Emil Tsalapatis <emil@etsalapatis.com>
v4 -> v5 (https://lore.kernel.org/bpf/20261109182055.7581-1-emil@etsalapatis.com/)
- Fix the buddy_alloc_memory() prototype in the per-CPU cache patch so
every intermediate patch compiles.
- Keep buddy_cache_alloc() and buddy_cache_free() static, and make
buddy_alloc_nogrow() use the per-CPU caches.
- Return 0 on arena_spin_trylock() success and -EBUSY on failure, and
use that convention directly in the irqsave wrapper.
- Increase the arena_spin_lock selftest's arena size for the larger
qnode array.
- Validate and poison objects before queuing deferred frees, and keep
deferred-drain errors from changing bulk-free ownership semantics.
- Prevent allocator growth in NMI context.
- Simplify the cache overflow test's prefill loop.
v3 -> v4 (https://lore.kernel.org/bpf/20261023002056.6647-1-emil@etsalapatis.com/)
- Rebase on bpf-next after the allocator compatibility fixes.
- Fix arena_spin_trylock() preemption pairing and add an irqsave variant.
- Raise the arena spinlock and per-CPU cache limits to 8192 (Harry).
- Use trylocks for the buddy and transfer locks in NMI context, allowing
cache refills and spills when they succeed and deferring frees on lock
failure. Detach the deferred list with one atomic exchange (Alexei).
- Move bulk spilling into the batch-refill patch, before the transfer
cache, and add the transfer-cache measurements to its commit log
(Alexei).
- Refill from existing chunks without growing the allocator, avoiding
virtual-address exhaustion when only individual objects fit (Alexei).
- Restore smp_store_release() and force buddy_cache_free() inline; the
measured free-latency difference disappears with inlining (Kumar).
- Address reviewer nits and clarify the cache footprint and free contracts.
v2 -> v3 (https://lore.kernel.org/bpf/20261021232641.10063-1-emil@etsalapatis.com/)
- Release the per-CPU cache claim with barrier() and WRITE_ONCE()
instead of smp_store_release(), since only contexts on the same CPU
claim the cache (Harry).
v1 -> v2 (https://lore.kernel.org/bpf/20261020181900.6755-1-emil@etsalapatis.com/)
- In the transfer cache, avoid taking two locks per free when it is full:
check the transfer array's count before taking its lock, and spill the
local batch with buddy_free_bulk() when the array is full or busy
(Alexei).
- Simplify the per-CPU caches: keep the guard in struct buddy_cache,
claim the cache once per miss or full cache with the refill and spill
paths out of line, keep the cache state in struct buddy, and move the
definitions into buddy.h (Alexei).
- Make buddy_free_bulk() fail only when nothing was freed (Alexei).
- Time frees in the benchmark, add a cross-CPU variant, and add before
and after numbers to the cover letter (Alexei).
- Addressed bot feedback.
Emil Tsalapatis (9):
selftests/bpf: libarena: Add bulk buddy allocation helpers
selftests/bpf: Add a parallel buddy allocator benchmark
selftests/bpf: libarena: Make spinlock trylock match arena_spin_unlock
selftests/bpf: libarena: Raise arena spinlock CPU limits
selftests/bpf: libarena: Add buddy per-CPU caches
selftests/bpf: libarena: Handle NMI reentrancy
selftests/bpf: libarena: Refill local caches in batches
selftests/bpf: libarena: Add a transfer cache
selftests/bpf: libarena: Cover buddy cache fast paths
tools/testing/selftests/bpf/bench.c | 4 +
.../selftests/bpf/benchs/bench_libarena.c | 118 +++-
.../bpf/benchs/run_bench_libarena.sh | 16 +
.../bpf/libarena/benchs/bench_malloc.bpf.c | 110 +++
.../libarena/include/bpf_arena_spin_lock.h | 42 +-
.../bpf/libarena/include/libarena/buddy.h | 58 ++
.../bpf/libarena/include/libarena/common.h | 4 +
.../bpf/libarena/selftests/test_buddy.bpf.c | 265 +++++++
.../selftests/bpf/libarena/src/buddy.bpf.c | 645 +++++++++++++++++-
.../selftests/bpf/progs/arena_spin_lock.c | 2 +-
10 files changed, 1209 insertions(+), 55 deletions(-)
--
2.52.0
^ permalink raw reply [flat|nested] 15+ messages in thread
* [PATCH bpf-next v5 1/9] selftests/bpf: libarena: Add bulk buddy allocation helpers
2026-11-10 0:38 [PATCH bpf-next v5 0/9] bpf libarena allocator improvements Emil Tsalapatis
@ 2026-11-10 0:38 ` Emil Tsalapatis
2026-11-10 0:38 ` [PATCH bpf-next v5 2/9] selftests/bpf: Add a parallel buddy allocator benchmark Emil Tsalapatis
` (8 subsequent siblings)
9 siblings, 0 replies; 15+ messages in thread
From: Emil Tsalapatis @ 2026-11-10 0:38 UTC (permalink / raw)
To: bpf; +Cc: ast, andrii, eddyz87, memxor, daniel, Emil Tsalapatis
The buddy allocator currently allocates and frees one object at a time.
Callers that need a group of equal-sized objects consequently take the
allocator lock and repeat the same split operations for every object.
Add buddy_alloc_bulk(), which obtains one block and divides it into a
power-of-two number of independently freeable objects. Add
buddy_free_bulk() to free an arbitrary array of objects while holding the
allocator lock once. Bulk frees cannot avoid coalescing each object
independently, but they still amortize lock acquisition.
buddy_free_bulk() fails only when its arguments are invalid or it cannot
take the allocator lock, in which case nothing was freed. Objects that
cannot be freed are reported individually, so a caller can tell from the
return value alone whether the array still owns its objects.
The per-CPU caches added later use this interface to populate a cache with
one trip through the central allocator.
Signed-off-by: Emil Tsalapatis <emil@etsalapatis.com>
---
.../bpf/libarena/include/libarena/buddy.h | 6 +
.../selftests/bpf/libarena/src/buddy.bpf.c | 148 ++++++++++++++----
2 files changed, 125 insertions(+), 29 deletions(-)
diff --git a/tools/testing/selftests/bpf/libarena/include/libarena/buddy.h b/tools/testing/selftests/bpf/libarena/include/libarena/buddy.h
index 90674ac077df..4c96307e2a2d 100644
--- a/tools/testing/selftests/bpf/libarena/include/libarena/buddy.h
+++ b/tools/testing/selftests/bpf/libarena/include/libarena/buddy.h
@@ -76,8 +76,14 @@ struct buddy {
int buddy_init(struct buddy __arena *buddy);
int buddy_destroy(struct buddy __arena *buddy);
int buddy_free(struct buddy __arena *buddy, void __arena *free);
+int buddy_free_bulk(struct buddy __arena *buddy, void __arena * __arena *addrs,
+ size_t naddrs);
void __arena *buddy_alloc(struct buddy __arena *buddy, size_t size);
void __arena *buddy_alloc_nogrow(struct buddy __arena *buddy, size_t size);
int buddy_grow(struct buddy __arena *buddy, size_t nr_chunks);
+int buddy_alloc_bulk(struct buddy __arena *buddy, size_t size, size_t norder,
+ void __arena * __arena *out);
+int buddy_alloc_bulk_nogrow(struct buddy __arena *buddy, size_t size, size_t norder,
+ void __arena * __arena *out);
#endif /* __BPF__ */
diff --git a/tools/testing/selftests/bpf/libarena/src/buddy.bpf.c b/tools/testing/selftests/bpf/libarena/src/buddy.bpf.c
index d12c8924a0ad..514d0a59eaff 100644
--- a/tools/testing/selftests/bpf/libarena/src/buddy.bpf.c
+++ b/tools/testing/selftests/bpf/libarena/src/buddy.bpf.c
@@ -628,7 +628,7 @@ __weak int buddy_destroy(struct buddy __arena *buddy)
return 0;
}
-__weak u64 buddy_chunk_alloc(struct buddy_chunk __arena *chunk, int order_req)
+__weak u64 buddy_chunk_alloc(struct buddy_chunk __arena *chunk, int order_req, int order_allocs)
{
struct buddy_header __arena *header, *tmp_header, *next_header;
u32 idx, tmpidx, retidx;
@@ -636,7 +636,7 @@ __weak u64 buddy_chunk_alloc(struct buddy_chunk __arena *chunk, int order_req)
u64 order = 0;
u64 i;
- for (order = order_req; order < BUDDY_CHUNK_NUM_ORDERS && can_loop; order++) {
+ for (order = order_req + order_allocs; order < BUDDY_CHUNK_NUM_ORDERS && can_loop; order++) {
if (chunk->freelists[order] != BUDDY_CHUNK_ITEMS)
break;
}
@@ -658,11 +658,19 @@ __weak u64 buddy_chunk_alloc(struct buddy_chunk __arena *chunk, int order_req)
header->prev_index = BUDDY_CHUNK_ITEMS;
header->next_index = BUDDY_CHUNK_ITEMS;
- if (idx_set_order(chunk, retidx, order_req))
- return (u64)NULL;
+ /*
+ * For a bulk allocation, record the allocation state and size
+ * of each item separately so each can be freed independently.
+ */
+ for (i = zero, idx = retidx; i < (1U << order_allocs) && can_loop; i++) {
+ if (idx_set_order(chunk, idx, order_req))
+ return (u64)NULL;
- if (idx_set_allocated(chunk, retidx, true))
- return (u64)NULL;
+ if (idx_set_allocated(chunk, idx, true))
+ return (u64)NULL;
+
+ idx += 1U << order_req;
+ }
/*
* Do not unpoison the address yet, will be done by the caller
@@ -673,7 +681,7 @@ __weak u64 buddy_chunk_alloc(struct buddy_chunk __arena *chunk, int order_req)
return (u64)NULL;
/* If we allocated from a larger-order chunk, split the buddies. */
- for (i = order_req; i < order && can_loop; i++) {
+ for (i = order_req + order_allocs; i < order && can_loop; i++) {
/*
* Flip the bit for the current order (the bit is guaranteed
* to be 0, so just add 1 << i).
@@ -706,14 +714,14 @@ __weak u64 buddy_chunk_alloc(struct buddy_chunk __arena *chunk, int order_req)
}
/* Scan the existing chunks for available memory. */
-static u64 buddy_alloc_from_existing_chunks(struct buddy __arena *buddy, int order)
+static u64 buddy_alloc_from_existing_chunks(struct buddy __arena *buddy, int order, int norder)
{
struct buddy_chunk __arena *chunk;
u64 address;
for (chunk = buddy->first_chunk; chunk != NULL && can_loop;
chunk = chunk->next) {
- address = buddy_chunk_alloc(chunk, order);
+ address = buddy_chunk_alloc(chunk, order, norder);
if (address)
return address;
}
@@ -725,7 +733,8 @@ static u64 buddy_alloc_from_existing_chunks(struct buddy __arena *buddy, int ord
* Try an allocation from a newly allocated chunk. Also
* incorporate the chunk into the linked list.
*/
-static u64 buddy_alloc_from_new_chunk(struct buddy __arena *buddy, struct buddy_chunk __arena *chunk, int order)
+static u64 buddy_alloc_from_new_chunk(struct buddy __arena *buddy, struct buddy_chunk __arena *chunk,
+ int order, int norder)
{
unsigned long flags;
u64 address;
@@ -733,7 +742,6 @@ static u64 buddy_alloc_from_new_chunk(struct buddy __arena *buddy, struct buddy_
if (buddy_lock(buddy, flags))
return (u64)NULL;
-
/*
* Add the chunk into the allocator and try
* to allocate specifically from that chunk.
@@ -741,34 +749,35 @@ static u64 buddy_alloc_from_new_chunk(struct buddy __arena *buddy, struct buddy_
chunk->next = buddy->first_chunk;
buddy->first_chunk = chunk;
- address = buddy_chunk_alloc(buddy->first_chunk, order);
+ address = buddy_chunk_alloc(buddy->first_chunk, order, norder);
buddy_unlock(buddy, flags);
return (u64)address;
}
-static __always_inline
-void __arena *buddy_alloc_maygrow(struct buddy __arena *buddy, size_t size, bool grow)
+static void __arena *buddy_alloc_memory(struct buddy __arena *buddy, size_t size,
+ int order, size_t norder, bool grow)
{
void __arena *address = NULL;
+ u8 __arena *object;
struct buddy_chunk __arena *chunk;
+ unsigned int alloc_size;
+ unsigned int i, offset;
unsigned long flags;
- int order;
if (!buddy)
return NULL;
- order = size_to_order(size);
- if (order >= BUDDY_CHUNK_NUM_ORDERS || order < 0) {
- arena_stderr("invalid order %d (sz %lu)\n", order, size);
+ if (order < 0 || order >= BUDDY_CHUNK_NUM_ORDERS || norder >= BUDDY_CHUNK_NUM_ORDERS - order) {
+ arena_stderr("invalid order %d (norder %lu) (sz %lu)\n", order, norder, size);
return NULL;
}
if (buddy_lock(buddy, flags))
return NULL;
- address = (u8 __arena *)buddy_alloc_from_existing_chunks(buddy, order);
+ address = (u8 __arena *)buddy_alloc_from_existing_chunks(buddy, order, norder);
buddy_unlock(buddy, flags);
if (address)
goto done;
@@ -779,23 +788,30 @@ void __arena *buddy_alloc_maygrow(struct buddy __arena *buddy, size_t size, bool
/* Get a new chunk. */
chunk = buddy_chunk_get(buddy);
if (chunk)
- address = (u8 __arena *)buddy_alloc_from_new_chunk(buddy, chunk, order);
+ address = (u8 __arena *)buddy_alloc_from_new_chunk(buddy, chunk, order, norder);
done:
/* If we failed to allocate memory, return NULL. */
if (!address)
return NULL;
+ alloc_size = BUDDY_MIN_ALLOC_BYTES << order;
+
/*
- * Unpoison exactly the amount of bytes requested. If the
- * data is smaller than the header, we must poison any
- * unused bytes that were part of the header.
+ * Unpoison exactly the requested bytes in each allocation. Keep
+ * the padding between the requested size and the rounded buddy
+ * allocation poisoned.
*/
- if (size < BUDDY_HEADER_OFF + sizeof(struct buddy_header __arena))
- asan_poison(address + BUDDY_HEADER_OFF, BUDDY_POISONED,
- sizeof(struct buddy_header __arena));
+ for (i = zero, offset = zero; i < (1U << norder) && can_loop; i++, offset += alloc_size) {
+ object = (u8 __arena *)address + offset;
- asan_unpoison(address, size);
+ if (size < BUDDY_HEADER_OFF + sizeof(struct buddy_header __arena))
+ asan_poison(object + BUDDY_HEADER_OFF,
+ BUDDY_POISONED,
+ sizeof(struct buddy_header __arena));
+
+ asan_unpoison(object, size);
+ }
return address;
}
@@ -803,13 +819,13 @@ void __arena *buddy_alloc_maygrow(struct buddy __arena *buddy, size_t size, bool
__weak
void __arena *buddy_alloc(struct buddy __arena *buddy, size_t size)
{
- return buddy_alloc_maygrow(buddy, size, true);
+ return buddy_alloc_memory(buddy, size, size_to_order(size), 0, true);
}
__weak
void __arena *buddy_alloc_nogrow(struct buddy __arena *buddy, size_t size)
{
- return buddy_alloc_maygrow(buddy, size, false);
+ return buddy_alloc_memory(buddy, size, size_to_order(size), 0, false);
}
__weak
@@ -838,6 +854,43 @@ int buddy_grow(struct buddy __arena *buddy, size_t nr_chunks)
return 0;
}
+static int buddy_alloc_bulk_memory(struct buddy __arena *buddy, size_t size, size_t norder,
+ void __arena * __arena *out, bool grow)
+{
+ void __arena *address;
+ unsigned int i, off;
+ unsigned int step;
+ int order;
+
+ if (!buddy || !out || norder >= BUDDY_CHUNK_NUM_ORDERS)
+ return -EINVAL;
+
+ order = size_to_order(size);
+ address = buddy_alloc_memory(buddy, size, order, norder, grow);
+ if (!address)
+ return -ENOMEM;
+
+ step = BUDDY_MIN_ALLOC_BYTES << order;
+ for (i = zero, off = zero; i < (1U << norder) && can_loop; i++, off += step)
+ out[i] = &address[off];
+
+ return 0;
+}
+
+__weak
+int buddy_alloc_bulk(struct buddy __arena *buddy, size_t size, size_t norder,
+ void __arena * __arena *out)
+{
+ return buddy_alloc_bulk_memory(buddy, size, norder, out, true);
+}
+
+__weak
+int buddy_alloc_bulk_nogrow(struct buddy __arena *buddy, size_t size, size_t norder,
+ void __arena * __arena *out)
+{
+ return buddy_alloc_bulk_memory(buddy, size, norder, out, false);
+}
+
static __always_inline int buddy_free_unlocked(struct buddy __arena *buddy, u64 addr)
{
struct buddy_header __arena *header, *buddy_header;
@@ -941,3 +994,40 @@ __weak int buddy_free(struct buddy __arena *buddy, void __arena *addr)
return ret;
}
+
+/*
+ * Fails only when the arguments are invalid or the buddy lock cannot be taken.
+ * Nothing is freed in either case. Objects that buddy_free_unlocked() rejects
+ * are reported there.
+ */
+__weak int buddy_free_bulk(struct buddy __arena *buddy, void __arena * __arena *addrs, size_t naddrs)
+{
+ unsigned long flags;
+ unsigned int i;
+ int ret;
+
+ if (!buddy)
+ return -EINVAL;
+
+ if (!naddrs)
+ return 0;
+
+ if (!addrs)
+ return -EINVAL;
+
+ ret = buddy_lock(buddy, flags);
+ if (ret)
+ return ret;
+
+ for (i = zero; i < naddrs && can_loop; i++) {
+ /* NULL pointers are ignored. */
+ if (!addrs[i])
+ continue;
+
+ buddy_free_unlocked(buddy, (u64)addrs[i]);
+ }
+
+ buddy_unlock(buddy, flags);
+
+ return 0;
+}
--
2.52.0
^ permalink raw reply related [flat|nested] 15+ messages in thread
* [PATCH bpf-next v5 2/9] selftests/bpf: Add a parallel buddy allocator benchmark
2026-11-10 0:38 [PATCH bpf-next v5 0/9] bpf libarena allocator improvements Emil Tsalapatis
2026-11-10 0:38 ` [PATCH bpf-next v5 1/9] selftests/bpf: libarena: Add bulk buddy allocation helpers Emil Tsalapatis
@ 2026-11-10 0:38 ` Emil Tsalapatis
2026-11-10 4:51 ` bot+bpf-ci
2026-11-10 0:38 ` [PATCH bpf-next v5 3/9] selftests/bpf: libarena: Make spinlock trylock match arena_spin_unlock Emil Tsalapatis
` (7 subsequent siblings)
9 siblings, 1 reply; 15+ messages in thread
From: Emil Tsalapatis @ 2026-11-10 0:38 UTC (permalink / raw)
To: bpf; +Cc: ast, andrii, eddyz87, memxor, daniel, Emil Tsalapatis
Add a benchmark for measuring buddy allocator latency under concurrent
load. Each worker performs a configurable number of allocations followed
by the corresponding frees, and the summary reports the mean latency of
allocations and frees separately. The frees chain the objects through
their first word, so the allocation size must be large enough to hold a
pointer.
Add a cross-CPU variant in which each worker publishes its list in a
shared slot, takes the list left there by the previous invocation and
frees it. With more than one pinned worker, most objects are then freed
on a different CPU from the one that allocated them.
Add both benchmarks to the libarena runner alongside the existing
single-threaded cases, sweeping the producer count from 1 to 8 with a
fixed allocation size and count to expose lock contention.
Report the existing libarena-malloc and libarena-calloc results in
ns/allocation and summarize them by total allocation count, so that all
libarena benchmarks share one output format. This replaces the previous
ns/op unit and the invocation and allocations-per-invocation fields.
Signed-off-by: Emil Tsalapatis <emil@etsalapatis.com>
---
tools/testing/selftests/bpf/bench.c | 4 +
.../selftests/bpf/benchs/bench_libarena.c | 118 ++++++++++++++++--
.../bpf/benchs/run_bench_libarena.sh | 16 +++
.../bpf/libarena/benchs/bench_malloc.bpf.c | 110 ++++++++++++++++
4 files changed, 237 insertions(+), 11 deletions(-)
diff --git a/tools/testing/selftests/bpf/bench.c b/tools/testing/selftests/bpf/bench.c
index d5f494184f9d..fbbd55399019 100644
--- a/tools/testing/selftests/bpf/bench.c
+++ b/tools/testing/selftests/bpf/bench.c
@@ -589,6 +589,8 @@ extern const struct bench bench_bpf_nop;
extern const struct bench bench_xdp_lb;
extern const struct bench bench_libarena_malloc;
extern const struct bench bench_libarena_calloc;
+extern const struct bench bench_libarena_malloc_free;
+extern const struct bench bench_libarena_malloc_xfree;
static const struct bench *benchs[] = {
&bench_count_global,
@@ -675,6 +677,8 @@ static const struct bench *benchs[] = {
&bench_xdp_lb,
&bench_libarena_malloc,
&bench_libarena_calloc,
+ &bench_libarena_malloc_free,
+ &bench_libarena_malloc_xfree,
};
static void find_benchmark(void)
diff --git a/tools/testing/selftests/bpf/benchs/bench_libarena.c b/tools/testing/selftests/bpf/benchs/bench_libarena.c
index 24e432244bf5..4096b54db520 100644
--- a/tools/testing/selftests/bpf/benchs/bench_libarena.c
+++ b/tools/testing/selftests/bpf/benchs/bench_libarena.c
@@ -25,6 +25,10 @@ static struct {
struct libarena_bench *skel;
int bench_fd;
int reset_fd;
+ bool reset_each_run;
+ bool stop;
+ int stopped;
+ int samples;
} ctx;
enum {
@@ -36,7 +40,7 @@ static const struct argp_option opts[] = {
{ "alloc_size", ARG_LIBARENA_ALLOC_SIZE, "BYTES", 0,
"Size of each arena allocation" },
{ "nallocs", ARG_LIBARENA_NALLOCS, "ITERS", 0,
- "Number of allocation per measurement" },
+ "Number of allocations per invocation" },
{},
};
@@ -68,12 +72,17 @@ const struct argp bench_libarena_argp = {
.parser = parse_arg,
};
-static void validate(void)
+static void validate_common(void)
{
if (env.consumer_cnt != 0) {
fprintf(stderr, "benchmark doesn't support consumers\n");
exit(1);
}
+}
+
+static void validate(void)
+{
+ validate_common();
if (env.producer_cnt != 1) {
fprintf(stderr, "benchmark supports exactly one producer\n");
@@ -81,6 +90,21 @@ static void validate(void)
}
}
+static void malloc_free_validate(void)
+{
+ validate_common();
+
+ if (env.producer_cnt < 1) {
+ fprintf(stderr, "benchmark requires at least one producer\n");
+ exit(1);
+ }
+
+ if (args.alloc_size < sizeof(void *)) {
+ fprintf(stderr, "allocation size must hold a pointer\n");
+ exit(1);
+ }
+}
+
static void setup_common(void)
{
struct arena_alloc_reserve_args reserve_args = {
@@ -114,38 +138,62 @@ static void setup_common(void)
ctx.skel->bss->bench_alloc_size = args.alloc_size;
ctx.skel->bss->bench_nallocs = args.nallocs;
ctx.reset_fd = bpf_program__fd(ctx.skel->progs.arena_buddy_reset);
+ ctx.reset_each_run = false;
+ ctx.stop = false;
+ ctx.stopped = 0;
+ ctx.samples = 0;
}
static void malloc_setup(void)
{
setup_common();
ctx.bench_fd = bpf_program__fd(ctx.skel->progs.bench_malloc);
+ ctx.reset_each_run = true;
}
static void calloc_setup(void)
{
setup_common();
ctx.bench_fd = bpf_program__fd(ctx.skel->progs.bench_calloc);
+ ctx.reset_each_run = true;
+}
+
+static void malloc_free_setup(void)
+{
+ setup_common();
+ ctx.bench_fd = bpf_program__fd(ctx.skel->progs.bench_malloc_free);
+ ctx.skel->bss->bench_collect = false;
+}
+
+static void malloc_xfree_setup(void)
+{
+ setup_common();
+ ctx.bench_fd = bpf_program__fd(ctx.skel->progs.bench_malloc_xfree);
+ ctx.skel->bss->bench_collect = false;
}
static void *producer(void *input)
{
int err;
- while (true) {
+ while (!__atomic_load_n(&ctx.stop, __ATOMIC_ACQUIRE)) {
err = libarena_run_prog(ctx.bench_fd);
if (err) {
fprintf(stderr, "libarena benchmark failed: %d\n", err);
exit(1);
}
- err = libarena_run_prog(ctx.reset_fd);
- if (err) {
- fprintf(stderr, "libarena alloc reset failed: %d\n", err);
- exit(1);
+ if (ctx.reset_each_run) {
+ err = libarena_run_prog(ctx.reset_fd);
+ if (err) {
+ fprintf(stderr, "libarena alloc reset failed: %d\n", err);
+ exit(1);
+ }
}
}
+ __atomic_add_fetch(&ctx.stopped, 1, __ATOMIC_RELEASE);
+
return NULL;
}
@@ -155,6 +203,13 @@ static void measure(struct bench_res *res)
res->hits = atomic_swap(&ctx.skel->bss->bench_hits, 0);
}
+static void malloc_free_measure(struct bench_res *res)
+{
+ if (++ctx.samples == env.warmup_sec)
+ __atomic_store_n(&ctx.skel->bss->bench_collect, true,
+ __ATOMIC_RELAXED);
+}
+
static void report_progress(int iter, struct bench_res *res, long delta_ns)
{
double latency_ns = 0.0;
@@ -162,7 +217,7 @@ static void report_progress(int iter, struct bench_res *res, long delta_ns)
if (res->hits)
latency_ns = res->duration_ns / (double)res->hits;
- printf("Iter %3d (%7.3lfus): latency %8.3lf ns/op (%ld allocations)\n",
+ printf("Iter %3d (%7.3lfus): latency %8.3lf ns/allocation (%ld allocations)\n",
iter, (delta_ns - 1000000000) / 1000.0, latency_ns, res->hits);
}
@@ -182,9 +237,30 @@ static void report_final(struct bench_res res[], int res_cnt)
return;
}
- printf("Summary: %.3lf ns/op, %.0lf invocations for %u allocations/invocation)\n",
- duration_ns / (double)hits, hits / (double)res_cnt,
- ctx.skel->bss->bench_nallocs);
+ printf("Summary: %.3lf ns/allocation (%ld allocations)\n",
+ duration_ns / (double)hits, hits);
+}
+
+static void malloc_free_report_final(struct bench_res res[], int res_cnt)
+{
+ long alloc_ns, free_ns, hits, free_hits;
+
+ __atomic_store_n(&ctx.stop, true, __ATOMIC_RELEASE);
+ while (__atomic_load_n(&ctx.stopped, __ATOMIC_ACQUIRE) != env.producer_cnt)
+ usleep(1000);
+
+ alloc_ns = __atomic_load_n(&ctx.skel->bss->bench_duration_ns, __ATOMIC_RELAXED);
+ free_ns = __atomic_load_n(&ctx.skel->bss->bench_free_duration_ns, __ATOMIC_RELAXED);
+ hits = __atomic_load_n(&ctx.skel->bss->bench_hits, __ATOMIC_RELAXED);
+ free_hits = __atomic_load_n(&ctx.skel->bss->bench_free_hits, __ATOMIC_RELAXED);
+
+ if (!hits || !free_hits) {
+ printf("Summary: no runs measured\n");
+ return;
+ }
+
+ printf("Summary: %.3lf ns/allocation, %.3lf ns/free (%ld allocations)\n",
+ alloc_ns / (double)hits, free_ns / (double)free_hits, hits);
}
const struct bench bench_libarena_malloc = {
@@ -208,3 +284,23 @@ const struct bench bench_libarena_calloc = {
.report_progress = report_progress,
.report_final = report_final,
};
+
+const struct bench bench_libarena_malloc_free = {
+ .name = "libarena-malloc-free",
+ .argp = &bench_libarena_argp,
+ .validate = malloc_free_validate,
+ .setup = malloc_free_setup,
+ .producer_thread = producer,
+ .measure = malloc_free_measure,
+ .report_final = malloc_free_report_final,
+};
+
+const struct bench bench_libarena_malloc_xfree = {
+ .name = "libarena-malloc-xfree",
+ .argp = &bench_libarena_argp,
+ .validate = malloc_free_validate,
+ .setup = malloc_xfree_setup,
+ .producer_thread = producer,
+ .measure = malloc_free_measure,
+ .report_final = malloc_free_report_final,
+};
diff --git a/tools/testing/selftests/bpf/benchs/run_bench_libarena.sh b/tools/testing/selftests/bpf/benchs/run_bench_libarena.sh
index 10afe4d52ebf..2dfa32cd95cc 100755
--- a/tools/testing/selftests/bpf/benchs/run_bench_libarena.sh
+++ b/tools/testing/selftests/bpf/benchs/run_bench_libarena.sh
@@ -29,3 +29,19 @@ printf "\t-------------------\n"
"$($RUN_BENCH --alloc_size "$size" --nallocs "$nallocs" libarena-calloc)"
done
done
+
+header "libarena parallel malloc/free\n"
+
+for producers in 1 2 4 8; do
+ summarize_libarena "$producers producers:" \
+ "$($RUN_BENCH -a -p "$producers" --alloc_size 64 --nallocs 1000 \
+ libarena-malloc-free)"
+done
+
+header "libarena parallel cross-CPU malloc/free\n"
+
+for producers in 1 2 4 8; do
+ summarize_libarena "$producers producers:" \
+ "$($RUN_BENCH -a -p "$producers" --alloc_size 64 --nallocs 1000 \
+ libarena-malloc-xfree)"
+done
diff --git a/tools/testing/selftests/bpf/libarena/benchs/bench_malloc.bpf.c b/tools/testing/selftests/bpf/libarena/benchs/bench_malloc.bpf.c
index 636178f1fa6a..de6d3d5c6bd7 100644
--- a/tools/testing/selftests/bpf/libarena/benchs/bench_malloc.bpf.c
+++ b/tools/testing/selftests/bpf/libarena/benchs/bench_malloc.bpf.c
@@ -9,6 +9,9 @@ u32 bench_alloc_size;
u32 bench_nallocs;
long bench_hits;
long bench_duration_ns;
+long bench_free_duration_ns;
+long bench_free_hits;
+bool bench_collect;
SEC("syscall")
int bench_malloc(void)
@@ -50,4 +53,111 @@ int bench_calloc(void)
return 0;
}
+struct bench_alloc {
+ struct bench_alloc __arena *next;
+};
+
+SEC("syscall")
+int bench_malloc_free(void)
+{
+ struct bench_alloc __arena *next, *obj, *head = NULL;
+ u64 alloc_ns = 0, free_ns = 0, start_ns;
+ u32 i, nallocs;
+ bool collect;
+
+ collect = READ_ONCE(bench_collect);
+ for (i = zero; i < bench_nallocs && can_loop; i++) {
+ start_ns = bpf_ktime_get_ns();
+ obj = arena_malloc(bench_alloc_size);
+ alloc_ns += bpf_ktime_get_ns() - start_ns;
+ if (!obj)
+ break;
+
+ obj->next = head;
+ head = obj;
+ }
+ nallocs = i;
+
+ for (i = zero; head && can_loop; i++) {
+ next = head->next;
+ start_ns = bpf_ktime_get_ns();
+ arena_free(head);
+ free_ns += bpf_ktime_get_ns() - start_ns;
+ head = next;
+ }
+
+ if (nallocs != bench_nallocs)
+ return -ENOMEM;
+
+ if (!collect)
+ return 0;
+
+ __sync_add_and_fetch(&bench_duration_ns, alloc_ns);
+ __sync_add_and_fetch(&bench_free_duration_ns, free_ns);
+ __sync_add_and_fetch(&bench_hits, nallocs);
+ __sync_add_and_fetch(&bench_free_hits, nallocs);
+ return 0;
+}
+
+/* List left behind by the last bench_malloc_xfree() invocation. */
+struct bench_alloc __arena *bench_xfree_head;
+
+SEC("syscall")
+int bench_malloc_xfree(void)
+{
+ struct bench_alloc __arena *next, *obj, *old, *cur, *head = NULL;
+ u64 alloc_ns = 0, free_ns = 0, start_ns;
+ u32 i, nallocs, nfrees;
+ bool collect;
+
+ collect = READ_ONCE(bench_collect);
+ for (i = zero; i < bench_nallocs && can_loop; i++) {
+ start_ns = bpf_ktime_get_ns();
+ obj = arena_malloc(bench_alloc_size);
+ alloc_ns += bpf_ktime_get_ns() - start_ns;
+ if (!obj)
+ break;
+
+ obj->next = head;
+ head = obj;
+ }
+ nallocs = i;
+
+ /*
+ * Publish this list and take the one left by the previous invocation,
+ * usually allocated by a worker on another CPU. If the exchange does
+ * not complete, free our own list instead.
+ */
+ old = READ_ONCE(bench_xfree_head);
+ while (can_loop) {
+ cur = cmpxchg(&bench_xfree_head, old, head);
+ if (cur == old) {
+ head = old;
+ break;
+ }
+ old = cur;
+ }
+
+ for (i = zero; head && can_loop; i++) {
+ next = head->next;
+ start_ns = bpf_ktime_get_ns();
+ arena_free(head);
+ free_ns += bpf_ktime_get_ns() - start_ns;
+ head = next;
+ }
+ nfrees = i;
+
+ if (nallocs != bench_nallocs)
+ return -ENOMEM;
+
+ if (!collect)
+ return 0;
+
+ __sync_add_and_fetch(&bench_duration_ns, alloc_ns);
+ __sync_add_and_fetch(&bench_free_duration_ns, free_ns);
+ __sync_add_and_fetch(&bench_hits, nallocs);
+ __sync_add_and_fetch(&bench_free_hits, nfrees);
+ return 0;
+}
+
char _license[] SEC("license") = "Dual BSD/GPL";
--
2.52.0
^ permalink raw reply related [flat|nested] 15+ messages in thread
* [PATCH bpf-next v5 3/9] selftests/bpf: libarena: Make spinlock trylock match arena_spin_unlock
2026-11-10 0:38 [PATCH bpf-next v5 0/9] bpf libarena allocator improvements Emil Tsalapatis
2026-11-10 0:38 ` [PATCH bpf-next v5 1/9] selftests/bpf: libarena: Add bulk buddy allocation helpers Emil Tsalapatis
2026-11-10 0:38 ` [PATCH bpf-next v5 2/9] selftests/bpf: Add a parallel buddy allocator benchmark Emil Tsalapatis
@ 2026-11-10 0:38 ` Emil Tsalapatis
2026-11-10 0:38 ` [PATCH bpf-next v5 4/9] selftests/bpf: libarena: Raise arena spinlock CPU limits Emil Tsalapatis
` (6 subsequent siblings)
9 siblings, 0 replies; 15+ messages in thread
From: Emil Tsalapatis @ 2026-11-10 0:38 UTC (permalink / raw)
To: bpf; +Cc: ast, andrii, eddyz87, memxor, daniel, Emil Tsalapatis
The arena_spin_trylock() macro is currently only used within the
lock slowpath, and so does not disable preemption. However, this
means it does not properly pair with arena_spin_unlock() that does.
Rename the existing function to arena_spin_trylock_raw() to denote
it just handles the test-and-test-and-set, and make a version of
the function that handles preemption. Also add an _irqsave() variant.
Fixes: 88d706ba7cc5 ("selftests/bpf: Introduce arena spin lock")
Signed-off-by: Emil Tsalapatis <emil@etsalapatis.com>
---
.../libarena/include/bpf_arena_spin_lock.h | 36 +++++++++++++++++--
1 file changed, 33 insertions(+), 3 deletions(-)
diff --git a/tools/testing/selftests/bpf/libarena/include/bpf_arena_spin_lock.h b/tools/testing/selftests/bpf/libarena/include/bpf_arena_spin_lock.h
index 055b5892cb63..c0eedb59986d 100644
--- a/tools/testing/selftests/bpf/libarena/include/bpf_arena_spin_lock.h
+++ b/tools/testing/selftests/bpf/libarena/include/bpf_arena_spin_lock.h
@@ -224,16 +224,35 @@ u32 arena_fetch_set_pending_acquire(arena_spinlock_t __arena *lock)
/**
* arena_spin_trylock - try to acquire the queued spinlock
* @lock : Pointer to queued spinlock structure
- * Return: 1 if lock acquired, 0 if failed
+ * Return: 0 if lock acquired, -EBUSY if failed
*/
static __always_inline int arena_spin_trylock(arena_spinlock_t __arena *lock)
{
int val = atomic_read(&lock->val);
+ if (unlikely(val))
+ return -EBUSY;
+
+ bpf_preempt_disable();
+ val = atomic_try_cmpxchg_acquire(&lock->val, &val, _Q_LOCKED_VAL);
+ if (unlikely(!val))
+ bpf_preempt_enable();
+
+ return val ? 0 : -EBUSY;
+}
+
+/*
+ * Trylock variant without preemption disable/reenable.
+ * Used within the lock slowpath.
+ */
+static __always_inline int arena_spin_trylock_raw(arena_spinlock_t __arena *lock)
+{
+ int val = atomic_read(&lock->val);
+
if (unlikely(val))
return 0;
- return likely(atomic_try_cmpxchg_acquire(&lock->val, &val, _Q_LOCKED_VAL));
+ return atomic_try_cmpxchg_acquire(&lock->val, &val, _Q_LOCKED_VAL);
}
__noinline __weak
@@ -346,7 +365,7 @@ int arena_spin_lock_slowpath(arena_spinlock_t __arena *lock, u32 val)
* attempt the trylock once more in the hope someone let go while we
* weren't watching.
*/
- if (arena_spin_trylock(lock))
+ if (arena_spin_trylock_raw(lock))
goto release;
/*
@@ -527,6 +546,17 @@ static __always_inline void arena_spin_unlock(arena_spinlock_t __arena *lock)
(__ret); \
})
+#define arena_spin_trylock_irqsave(lock, flags) \
+ ({ \
+ int __ret; \
+ bpf_local_irq_save(&(flags)); \
+ __ret = arena_spin_trylock((lock)); \
+ if (__ret) \
+ bpf_local_irq_restore(&(flags)); \
+ (__ret); \
+ })
+
+
#define arena_spin_unlock_irqrestore(lock, flags) \
({ \
arena_spin_unlock((lock)); \
--
2.52.0
^ permalink raw reply related [flat|nested] 15+ messages in thread
* [PATCH bpf-next v5 4/9] selftests/bpf: libarena: Raise arena spinlock CPU limits
2026-11-10 0:38 [PATCH bpf-next v5 0/9] bpf libarena allocator improvements Emil Tsalapatis
` (2 preceding siblings ...)
2026-11-10 0:38 ` [PATCH bpf-next v5 3/9] selftests/bpf: libarena: Make spinlock trylock match arena_spin_unlock Emil Tsalapatis
@ 2026-11-10 0:38 ` Emil Tsalapatis
2026-11-10 0:38 ` [PATCH bpf-next v5 5/9] selftests/bpf: libarena: Add buddy per-CPU caches Emil Tsalapatis
` (5 subsequent siblings)
9 siblings, 0 replies; 15+ messages in thread
From: Emil Tsalapatis @ 2026-11-10 0:38 UTC (permalink / raw)
To: bpf; +Cc: ast, andrii, eddyz87, memxor, daniel, Emil Tsalapatis
Bump the maximum number of CPUs supported by the arena spinlocks
to 8K, up from 1K.
Signed-off-by: Emil Tsalapatis <emil@etsalapatis.com>
---
.../selftests/bpf/libarena/include/bpf_arena_spin_lock.h | 6 +++---
tools/testing/selftests/bpf/progs/arena_spin_lock.c | 2 +-
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/tools/testing/selftests/bpf/libarena/include/bpf_arena_spin_lock.h b/tools/testing/selftests/bpf/libarena/include/bpf_arena_spin_lock.h
index c0eedb59986d..8422c0939c83 100644
--- a/tools/testing/selftests/bpf/libarena/include/bpf_arena_spin_lock.h
+++ b/tools/testing/selftests/bpf/libarena/include/bpf_arena_spin_lock.h
@@ -77,7 +77,7 @@ struct arena_qnode {
* 16-17: tail index
* 18-31: tail cpu (+1)
*/
-#define _Q_MAX_CPUS 1024
+#define _Q_MAX_CPUS 8192
#define _Q_SET_MASK(type) (((1U << _Q_ ## type ## _BITS) - 1)\
<< _Q_ ## type ## _OFFSET)
@@ -109,7 +109,7 @@ static inline u32 encode_tail(int cpu, int idx)
{
u32 tail;
- tail = (cpu + 1) << _Q_TAIL_CPU_OFFSET;
+ tail = (u32)(cpu + 1) << _Q_TAIL_CPU_OFFSET;
tail |= idx << _Q_TAIL_IDX_OFFSET; /* assume < 4 */
return tail;
@@ -509,7 +509,7 @@ static __always_inline int arena_spin_lock(arena_spinlock_t __arena *lock)
{
int val = 0;
- if (CONFIG_NR_CPUS > 1024)
+ if (CONFIG_NR_CPUS > _Q_MAX_CPUS)
return -EOPNOTSUPP;
bpf_preempt_disable();
diff --git a/tools/testing/selftests/bpf/progs/arena_spin_lock.c b/tools/testing/selftests/bpf/progs/arena_spin_lock.c
index 92e75ec3844c..9e91e562a4ea 100644
--- a/tools/testing/selftests/bpf/progs/arena_spin_lock.c
+++ b/tools/testing/selftests/bpf/progs/arena_spin_lock.c
@@ -10,7 +10,7 @@
struct {
__uint(type, BPF_MAP_TYPE_ARENA);
__uint(map_flags, BPF_F_MMAPABLE);
- __uint(max_entries, 100); /* number of pages */
+ __uint(max_entries, 256); /* number of pages, including qnodes */
#ifdef __TARGET_ARCH_arm64
__ulong(map_extra, 0x1ull << 32); /* start of mmap() region */
#else
--
2.52.0
^ permalink raw reply related [flat|nested] 15+ messages in thread
* [PATCH bpf-next v5 5/9] selftests/bpf: libarena: Add buddy per-CPU caches
2026-11-10 0:38 [PATCH bpf-next v5 0/9] bpf libarena allocator improvements Emil Tsalapatis
` (3 preceding siblings ...)
2026-11-10 0:38 ` [PATCH bpf-next v5 4/9] selftests/bpf: libarena: Raise arena spinlock CPU limits Emil Tsalapatis
@ 2026-11-10 0:38 ` Emil Tsalapatis
2026-11-10 0:38 ` [PATCH bpf-next v5 6/9] selftests/bpf: libarena: Handle NMI reentrancy Emil Tsalapatis
` (4 subsequent siblings)
9 siblings, 0 replies; 15+ messages in thread
From: Emil Tsalapatis @ 2026-11-10 0:38 UTC (permalink / raw)
To: bpf; +Cc: ast, andrii, eddyz87, memxor, daniel, Emil Tsalapatis
Every small allocation currently reaches the central buddy allocator and
takes its lock. This serializes otherwise independent callers and touches
the buddy metadata on every allocation and free.
Add a fixed-size LIFO array for each cached order and CPU. Keep the
per-CPU cache pointers in struct buddy and allocate each CPU's packed
cache from the buddy during initialization. Cache the eight smallest
allocation orders, up to 2 KiB. Each array holds 16 objects, retaining up
to 63.75 KiB of objects per CPU. The cache metadata occupies 1600 bytes
and consumes a 2 KiB buddy allocation per CPU.
Allocations pop from the local array and frees push onto it without taking
the buddy lock. Empty and full arrays fall through to the central buddy
allocator. Packed arrays keep the fast path to a bounds check and a single
slot access.
BPF programs only disable migration, so another task can preempt a cache
operation and run on the same CPU, and an IRQ or NMI program can interrupt
one. Claim the CPU with a cmpxchg on a per-CPU flag before touching its
arrays, following the model used by the main BPF memory allocator. An
operation that finds the CPU already claimed falls through to the central
buddy allocator.
Signed-off-by: Emil Tsalapatis <emil@etsalapatis.com>
---
.../bpf/libarena/include/libarena/buddy.h | 34 +++
.../bpf/libarena/include/libarena/common.h | 4 +
.../bpf/libarena/selftests/test_buddy.bpf.c | 9 +
.../selftests/bpf/libarena/src/buddy.bpf.c | 233 +++++++++++++++++-
4 files changed, 273 insertions(+), 7 deletions(-)
diff --git a/tools/testing/selftests/bpf/libarena/include/libarena/buddy.h b/tools/testing/selftests/bpf/libarena/include/libarena/buddy.h
index 4c96307e2a2d..ec5e79a07540 100644
--- a/tools/testing/selftests/bpf/libarena/include/libarena/buddy.h
+++ b/tools/testing/selftests/bpf/libarena/include/libarena/buddy.h
@@ -65,10 +65,44 @@ struct buddy_chunk {
struct buddy_chunk __arena *next;
};
+/* Constants related to the buddy allocator's object caches. */
+enum buddy_cache_consts {
+ /*
+ * Do not cache larger allocations to avoid excessive
+ * memory consumption from the per-cpu lists.
+ */
+ BUDDY_CACHE_NUM_ORDERS = 1 << 3,
+
+ /*
+ * Just like arena spinlocks, hardcode percpu state to a maximum number of CPUs.
+ * Since arena spinlocks are a hard dependency, use the exact same limit.
+ */
+ BUDDY_CACHE_MAX_CPUS = 8192,
+
+ /*
+ * Objects retained locally for each allocation order.
+ */
+ BUDDY_CACHE_CAPACITY = 16,
+};
+
+struct buddy_cache_order {
+ u32 elems;
+ u32 __pad;
+ void __arena *objects[BUDDY_CACHE_CAPACITY];
+} __attribute__((aligned(64)));
+
+/* Dynamically allocated packed per-CPU slot slab. */
+struct buddy_cache {
+ /* Claimed by an in-flight cache operation on this CPU. */
+ unsigned int active;
+ struct buddy_cache_order orders[BUDDY_CACHE_NUM_ORDERS];
+};
+
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];
};
#ifdef __BPF__
diff --git a/tools/testing/selftests/bpf/libarena/include/libarena/common.h b/tools/testing/selftests/bpf/libarena/include/libarena/common.h
index 940e82cc2928..62a49dcf31d6 100644
--- a/tools/testing/selftests/bpf/libarena/include/libarena/common.h
+++ b/tools/testing/selftests/bpf/libarena/include/libarena/common.h
@@ -22,6 +22,10 @@
#define __maybe_unused __attribute__((__unused__))
#endif
+#ifndef min
+#define min(a, b) ((a) < (b) ? (a) : (b))
+#endif
+
#define private(name) SEC(".data." #name) __hidden __attribute__((aligned(8)))
#define ARENA_PAGES (1UL << (32 - __builtin_ffs(__PAGE_SIZE) + 1))
diff --git a/tools/testing/selftests/bpf/libarena/selftests/test_buddy.bpf.c b/tools/testing/selftests/bpf/libarena/selftests/test_buddy.bpf.c
index 5628f0987012..85a7ea8b2e97 100644
--- a/tools/testing/selftests/bpf/libarena/selftests/test_buddy.bpf.c
+++ b/tools/testing/selftests/bpf/libarena/selftests/test_buddy.bpf.c
@@ -34,9 +34,18 @@ __weak int test_buddy_create(void)
if (ret)
return ret;
+#ifndef BPF_ARENA_ASAN
+ if (!buddy.percpu[0]) {
+ buddy_destroy(&buddy);
+ return -EINVAL;
+ }
+#endif
+
ret = buddy_destroy(&buddy);
if (ret)
return ret;
+ if (buddy.percpu[0])
+ return -EINVAL;
}
return 0;
diff --git a/tools/testing/selftests/bpf/libarena/src/buddy.bpf.c b/tools/testing/selftests/bpf/libarena/src/buddy.bpf.c
index 514d0a59eaff..58e666210fdb 100644
--- a/tools/testing/selftests/bpf/libarena/src/buddy.bpf.c
+++ b/tools/testing/selftests/bpf/libarena/src/buddy.bpf.c
@@ -7,6 +7,9 @@
#include <bpf_arena_spin_lock.h>
+_Static_assert(BUDDY_CACHE_MAX_CPUS == _Q_MAX_CPUS,
+ "buddy caches must cover every CPU arena spinlocks support");
+
/*
* Buddy allocator arena-based implementation.
*
@@ -38,18 +41,162 @@
* and their allocation state. It also includes the freelist
* heads for the allocation itself.
*/
+static u8 idx_get_order(struct buddy_chunk __arena *chunk, u64 idx);
+static int idx_is_allocated(struct buddy_chunk __arena *chunk, u64 idx, bool *allocated);
+static u64 arena_next_pow2(__u64 n);
+static u64 size_to_order(size_t size);
+static void __arena *buddy_alloc_memory(struct buddy __arena *buddy, size_t size,
+ int order, size_t norder, bool grow);
+static int buddy_free_unlocked(struct buddy __arena *buddy, u64 addr);
+
+#define buddy_lock(buddy, flags) (arena_spin_lock_irqsave(&(buddy)->lock, (flags)))
+#define buddy_unlock(buddy, flags) (arena_spin_unlock_irqrestore(&(buddy)->lock, (flags)))
+
+/*
+ * Per-CPU caching is disabled in ASAN builds because its additional call-chain
+ * stack usage causes instrumented programs to exceed BPF's 512-byte stack limit.
+ */
+#ifdef BPF_ARENA_ASAN
+#define buddy_order_cached(order) false
+#else
+#define buddy_order_cached(order) ((order) < BUDDY_CACHE_NUM_ORDERS)
+#endif
+
+static __always_inline struct buddy_cache __arena *
+buddy_cache_enter(struct buddy __arena *buddy)
+{
+ struct buddy_cache __arena *cache;
+
+ /* The caches only exist between buddy_init() and buddy_destroy(). */
+ cache = buddy->percpu[bpf_get_smp_processor_id()];
+ if (unlikely(!cache))
+ return NULL;
+
+ /*
+ * No support for cache operations from contexts that interrupted or
+ * preempted an in-flight operation on the same CPU, just like the
+ * kernel's BPF memory allocator.
+ */
+ if (cmpxchg(&cache->active, 0, 1))
+ return NULL;
+
+ return cache;
+}
+
+static __always_inline void buddy_cache_exit(struct buddy_cache __arena *cache)
+{
+ smp_store_release(&cache->active, 0);
+}
+
+static __always_inline void __arena *
+buddy_cache_pop(struct buddy_cache __arena *cache, int order)
+{
+ struct buddy_cache_order __arena *slots;
+ void __arena *addr;
+ u32 elems;
+
+ slots = &cache->orders[order];
+ elems = READ_ONCE(slots->elems);
+ if (!elems)
+ return NULL;
+
+ addr = READ_ONCE(slots->objects[elems - 1]);
+ WRITE_ONCE(slots->elems, elems - 1);
+
+ return addr;
+}
+
+static __always_inline int buddy_cache_push(struct buddy_cache __arena *cache,
+ int order, void __arena *obj)
+{
+ struct buddy_cache_order __arena *slots;
+ u32 elems;
+
+ slots = &cache->orders[order];
+ elems = READ_ONCE(slots->elems);
+ if (elems >= BUDDY_CACHE_CAPACITY)
+ return -ENOMEM;
+
+ WRITE_ONCE(slots->objects[elems], obj);
+ WRITE_ONCE(slots->elems, elems + 1);
+
+ return 0;
+}
+
+static void __arena *buddy_cache_alloc(struct buddy __arena *buddy, size_t order)
+{
+ struct buddy_cache __arena *cache;
+ void __arena *address = NULL;
+
+ if (!buddy_order_cached(order))
+ return NULL;
+
+ cache = buddy_cache_enter(buddy);
+ if (!cache)
+ return NULL;
+
+ address = buddy_cache_pop(cache, order);
+ buddy_cache_exit(cache);
+
+ return address;
+}
+
+/*
+ * Retrieve the order of an allocation from the address. Reject addresses
+ * that do not start an allocated block so that the locked path can report
+ * them, instead of caching memory that overlaps a live allocation. Cached
+ * objects stay marked as allocated, so freeing an object that is already
+ * cached is not detected.
+ */
+static int buddy_cache_get_order(void __arena *addr)
+{
+ struct buddy_chunk __arena *chunk;
+ bool allocated;
+ u64 idx;
+
+ if ((u64)addr & (BUDDY_MIN_ALLOC_BYTES - 1))
+ return BUDDY_CHUNK_NUM_ORDERS;
+
+ chunk = (void __arena *)((u64)addr & ~BUDDY_CHUNK_OFFSET_MASK);
+ idx = ((u64)addr & BUDDY_CHUNK_OFFSET_MASK) / BUDDY_MIN_ALLOC_BYTES;
+
+ if (idx_is_allocated(chunk, idx, &allocated) || !allocated)
+ return BUDDY_CHUNK_NUM_ORDERS;
+
+ return idx_get_order(chunk, idx);
+}
+
+static __always_inline int buddy_cache_free(struct buddy __arena *buddy, void __arena *addr)
+{
+ struct buddy_cache __arena *cache;
+ int order;
+ int ret = 0;
+
+ /* Skip the metadata lookup when caching is compiled out. */
+ if (!buddy_order_cached(0))
+ return -EINVAL;
+
+ order = buddy_cache_get_order(addr);
+ if (!buddy_order_cached(order))
+ return -EINVAL;
+ cache = buddy_cache_enter(buddy);
+ if (!cache)
+ return -EAGAIN;
+
+ ret = buddy_cache_push(cache, order, addr);
+ buddy_cache_exit(cache);
+
+ return ret;
+}
enum {
BUDDY_POISONED = (s8)0xef,
/* Number of pages to be allocated per chunk. */
- BUDDY_CHUNK_PAGES = BUDDY_CHUNK_BYTES / __PAGE_SIZE
+ BUDDY_CHUNK_PAGES = BUDDY_CHUNK_BYTES / __PAGE_SIZE,
};
-#define buddy_lock(buddy, flags) (arena_spin_lock_irqsave(&(buddy)->lock, (flags)))
-#define buddy_unlock(buddy, flags) (arena_spin_unlock_irqrestore(&(buddy)->lock, (flags)))
-
/*
* Reserve part of the arena address space for the allocator. We use
* this to get aligned addresses for the chunks, since the arena
@@ -557,6 +704,41 @@ static struct buddy_chunk __arena *buddy_chunk_get(struct buddy __arena *buddy)
return chunk;
}
+static int buddy_cache_init(struct buddy __arena *buddy)
+{
+ struct buddy_cache __arena *cache;
+ unsigned int cpu, order;
+ int ret;
+
+ if (!buddy_order_cached(0))
+ return 0;
+
+ if (CONFIG_NR_CPUS > BUDDY_CACHE_MAX_CPUS)
+ return -E2BIG;
+
+ for (cpu = zero; cpu < CONFIG_NR_CPUS && can_loop; cpu++) {
+ ret = buddy_alloc_bulk(buddy, sizeof(*cache), 0,
+ (void __arena * __arena *)&buddy->percpu[cpu]);
+ if (ret)
+ return ret;
+
+ cache = buddy->percpu[cpu];
+ WRITE_ONCE(cache->active, 0);
+ for (order = zero; order < BUDDY_CACHE_NUM_ORDERS && can_loop; order++)
+ WRITE_ONCE(cache->orders[order].elems, 0);
+ }
+
+ return 0;
+}
+
+static void buddy_cache_destroy(struct buddy __arena *buddy)
+{
+ unsigned int cpu;
+
+ for (cpu = zero; cpu < min(CONFIG_NR_CPUS, BUDDY_CACHE_MAX_CPUS) && can_loop; cpu++)
+ WRITE_ONCE(buddy->percpu[cpu], NULL);
+}
+
__weak int buddy_init(struct buddy __arena *buddy)
{
struct buddy_chunk __arena *chunk;
@@ -590,7 +772,16 @@ __weak int buddy_init(struct buddy __arena *buddy)
buddy_unlock(buddy, flags);
- return chunk ? 0 : -ENOMEM;
+ if (!chunk)
+ return -ENOMEM;
+
+ ret = buddy_cache_init(buddy);
+ if (ret) {
+ buddy_destroy(buddy);
+ return ret;
+ }
+
+ return 0;
}
/*
@@ -606,6 +797,8 @@ __weak int buddy_destroy(struct buddy __arena *buddy)
if (!buddy)
return -EINVAL;
+ buddy_cache_destroy(buddy);
+
/*
* Traverse all buddy chunks and free them back to the arena
* with the same granularity they were allocated with.
@@ -819,13 +1012,35 @@ static void __arena *buddy_alloc_memory(struct buddy __arena *buddy, size_t size
__weak
void __arena *buddy_alloc(struct buddy __arena *buddy, size_t size)
{
- return buddy_alloc_memory(buddy, size, size_to_order(size), 0, true);
+ void __arena *address;
+ int order;
+
+ if (!buddy)
+ return NULL;
+
+ order = size_to_order(size);
+ address = buddy_cache_alloc(buddy, order);
+ if (address)
+ return address;
+
+ return buddy_alloc_memory(buddy, size, order, 0, true);
}
__weak
void __arena *buddy_alloc_nogrow(struct buddy __arena *buddy, size_t size)
{
- return buddy_alloc_memory(buddy, size, size_to_order(size), 0, false);
+ void __arena *address;
+ int order;
+
+ if (!buddy)
+ return NULL;
+
+ order = size_to_order(size);
+ address = buddy_cache_alloc(buddy, order);
+ if (address)
+ return address;
+
+ return buddy_alloc_memory(buddy, size, order, 0, false);
}
__weak
@@ -984,6 +1199,10 @@ __weak int buddy_free(struct buddy __arena *buddy, void __arena *addr)
if (!addr)
return 0;
+ ret = buddy_cache_free(buddy, addr);
+ if (!ret)
+ return 0;
+
ret = buddy_lock(buddy, flags);
if (ret)
return ret;
--
2.52.0
^ permalink raw reply related [flat|nested] 15+ messages in thread
* [PATCH bpf-next v5 6/9] selftests/bpf: libarena: Handle NMI reentrancy
2026-11-10 0:38 [PATCH bpf-next v5 0/9] bpf libarena allocator improvements Emil Tsalapatis
` (4 preceding siblings ...)
2026-11-10 0:38 ` [PATCH bpf-next v5 5/9] selftests/bpf: libarena: Add buddy per-CPU caches Emil Tsalapatis
@ 2026-11-10 0:38 ` Emil Tsalapatis
2026-11-10 1:34 ` sashiko-bot
2026-11-10 4:51 ` bot+bpf-ci
2026-11-10 0:38 ` [PATCH bpf-next v5 7/9] selftests/bpf: libarena: Refill local caches in batches Emil Tsalapatis
` (3 subsequent siblings)
9 siblings, 2 replies; 15+ messages in thread
From: Emil Tsalapatis @ 2026-11-10 0:38 UTC (permalink / raw)
To: bpf; +Cc: ast, andrii, eddyz87, memxor, daniel, Emil Tsalapatis
An NMI can interrupt the buddy allocator while it holds the buddy lock, so
code running in NMI context must not wait for that lock.
Use trylock for buddy operations in NMI context and normal locking in other
contexts. If the buddy lock cannot be acquired, allocations return NULL
and frees transfer ownership to an intrusive deferred-free list.
Drain deferred frees on a later successful lock acquisition, including
from NMI context.
Signed-off-by: Emil Tsalapatis <emil@etsalapatis.com>
---
.../bpf/libarena/include/libarena/buddy.h | 3 +
.../bpf/libarena/selftests/test_buddy.bpf.c | 2 +
.../selftests/bpf/libarena/src/buddy.bpf.c | 135 ++++++++++++++++--
3 files changed, 131 insertions(+), 9 deletions(-)
diff --git a/tools/testing/selftests/bpf/libarena/include/libarena/buddy.h b/tools/testing/selftests/bpf/libarena/include/libarena/buddy.h
index ec5e79a07540..7553ad5a879d 100644
--- a/tools/testing/selftests/bpf/libarena/include/libarena/buddy.h
+++ b/tools/testing/selftests/bpf/libarena/include/libarena/buddy.h
@@ -49,6 +49,8 @@ struct buddy_header {
u32 next_index; /* Same for the next allocation. */
};
+struct buddy_deferred_free;
+
/*
* We bring memory into the allocator 1 MiB at a time.
*/
@@ -103,6 +105,7 @@ struct buddy {
arena_spinlock_t lock; /* Allocator lock */
u64 vaddr; /* Allocation into reserved vaddr */
struct buddy_cache __arena *percpu[BUDDY_CACHE_MAX_CPUS];
+ struct buddy_deferred_free __arena *deferred_free;
};
#ifdef __BPF__
diff --git a/tools/testing/selftests/bpf/libarena/selftests/test_buddy.bpf.c b/tools/testing/selftests/bpf/libarena/selftests/test_buddy.bpf.c
index 85a7ea8b2e97..e7c6ac76433c 100644
--- a/tools/testing/selftests/bpf/libarena/selftests/test_buddy.bpf.c
+++ b/tools/testing/selftests/bpf/libarena/selftests/test_buddy.bpf.c
@@ -44,6 +44,8 @@ __weak int test_buddy_create(void)
ret = buddy_destroy(&buddy);
if (ret)
return ret;
+ if (buddy.deferred_free)
+ return -EINVAL;
if (buddy.percpu[0])
return -EINVAL;
}
diff --git a/tools/testing/selftests/bpf/libarena/src/buddy.bpf.c b/tools/testing/selftests/bpf/libarena/src/buddy.bpf.c
index 58e666210fdb..7fd74f4c5052 100644
--- a/tools/testing/selftests/bpf/libarena/src/buddy.bpf.c
+++ b/tools/testing/selftests/bpf/libarena/src/buddy.bpf.c
@@ -6,6 +6,7 @@
#include <libarena/buddy.h>
#include <bpf_arena_spin_lock.h>
+#include <bpf_experimental.h>
_Static_assert(BUDDY_CACHE_MAX_CPUS == _Q_MAX_CPUS,
"buddy caches must cover every CPU arena spinlocks support");
@@ -48,8 +49,12 @@ static u64 size_to_order(size_t size);
static void __arena *buddy_alloc_memory(struct buddy __arena *buddy, size_t size,
int order, size_t norder, bool grow);
static int buddy_free_unlocked(struct buddy __arena *buddy, u64 addr);
+static bool buddy_deferred_free_pending(struct buddy __arena *buddy);
+static void buddy_deferred_free_complete(struct buddy __arena *buddy);
-#define buddy_lock(buddy, flags) (arena_spin_lock_irqsave(&(buddy)->lock, (flags)))
+#define buddy_lock(buddy, flags) \
+ (bpf_in_nmi() ? arena_spin_trylock_irqsave(&(buddy)->lock, (flags)) : \
+ arena_spin_lock_irqsave(&(buddy)->lock, (flags)))
#define buddy_unlock(buddy, flags) (arena_spin_unlock_irqrestore(&(buddy)->lock, (flags)))
/*
@@ -747,6 +752,7 @@ __weak int buddy_init(struct buddy __arena *buddy)
if (!asan_ready())
return -EINVAL;
+ buddy->deferred_free = NULL;
/* Reserve enough address space to ensure allocations are aligned. */
ret = buddy_reserve_arena_vaddr(buddy);
@@ -817,6 +823,7 @@ __weak int buddy_destroy(struct buddy __arena *buddy)
/* Clear all fields. */
buddy->first_chunk = NULL;
+ buddy->deferred_free = NULL;
return 0;
}
@@ -970,12 +977,16 @@ static void __arena *buddy_alloc_memory(struct buddy __arena *buddy, size_t size
if (buddy_lock(buddy, flags))
return NULL;
+ if (unlikely(buddy_deferred_free_pending(buddy))) {
+ buddy_deferred_free_complete(buddy);
+ }
+
address = (u8 __arena *)buddy_alloc_from_existing_chunks(buddy, order, norder);
buddy_unlock(buddy, flags);
if (address)
goto done;
- if (!grow)
+ if (!grow || bpf_in_nmi())
return NULL;
/* Get a new chunk. */
@@ -1050,6 +1061,9 @@ int buddy_grow(struct buddy __arena *buddy, size_t nr_chunks)
unsigned long flags;
size_t chunk_index;
+ if (bpf_in_nmi())
+ return -EOPNOTSUPP;
+
for (chunk_index = zero; chunk_index < nr_chunks && can_loop; chunk_index++) {
chunk = buddy_chunk_get(buddy);
if (!chunk)
@@ -1187,6 +1201,98 @@ static __always_inline int buddy_free_unlocked(struct buddy __arena *buddy, u64
return 0;
}
+static bool buddy_deferred_free_pending(struct buddy __arena *buddy)
+{
+ return buddy->deferred_free != NULL;
+}
+
+static __always_inline void buddy_deferred_free_push(struct buddy __arena *buddy,
+ void __arena *first, void __arena *last)
+{
+ void __arena *head;
+
+ do {
+ head = READ_ONCE(buddy->deferred_free);
+ *(void __arena * __arena *)last = head;
+ } while (cmpxchg((void __arena * __arena *)&buddy->deferred_free,
+ head, first) != head && can_loop);
+}
+
+static int buddy_deferred_free_start(struct buddy __arena *buddy, void __arena *addr)
+{
+ int order = buddy_cache_get_order(addr);
+
+ if (order >= BUDDY_CHUNK_NUM_ORDERS)
+ return -EINVAL;
+
+ /*
+ * Make a linked list by chaining the allocations into a linked list.
+ * Possible because allocations are 16 bytes minimum and so fit a pointer.
+ */
+ asan_poison(addr, BUDDY_POISONED, BUDDY_MIN_ALLOC_BYTES << order);
+ asan_unpoison(addr, sizeof(void *));
+ buddy_deferred_free_push(buddy, addr, addr);
+ return 0;
+}
+
+static int buddy_deferred_free_start_bulk(struct buddy __arena *buddy,
+ void __arena * __arena *addrs, size_t naddrs)
+{
+ void __arena *first, *last, *addr;
+ unsigned int i;
+ int ret = 0, order;
+
+ /*
+ * Chain the allocations into a local linked list. Possible because
+ * allocations are 16 bytes minimum and so fit a pointer.
+ */
+ first = NULL;
+ last = NULL;
+ for (i = zero; i < naddrs && can_loop; i++) {
+ addr = addrs[i];
+ if (!addr)
+ continue;
+
+ order = buddy_cache_get_order(addr);
+ if (order >= BUDDY_CHUNK_NUM_ORDERS) {
+ ret = -EINVAL;
+ continue;
+ }
+
+ asan_poison(addr, BUDDY_POISONED, BUDDY_MIN_ALLOC_BYTES << order);
+ asan_unpoison(addr, sizeof(void *));
+
+ if (!first)
+ first = addr;
+ else
+ *(void __arena * __arena *)last = addr;
+
+ last = addr;
+ }
+
+ if (!first)
+ return ret;
+
+ /* Splice the local list onto the deferred-free list. */
+ buddy_deferred_free_push(buddy, first, last);
+ return 0;
+}
+
+static void buddy_deferred_free_complete(struct buddy __arena *buddy)
+{
+ void __arena *head, *old;
+
+ head = __sync_lock_test_and_set((void __arena * __arena *)&buddy->deferred_free,
+ NULL);
+
+ while (head && can_loop) {
+ old = head;
+ head = *(void __arena * __arena *)head;
+
+ buddy_free_unlocked(buddy, (u64)old);
+ }
+}
+
__weak int buddy_free(struct buddy __arena *buddy, void __arena *addr)
{
unsigned long flags;
@@ -1204,20 +1310,26 @@ __weak int buddy_free(struct buddy __arena *buddy, void __arena *addr)
return 0;
ret = buddy_lock(buddy, flags);
- if (ret)
- return ret;
+ if (ret) {
+ return buddy_deferred_free_start(buddy, addr);
+ }
ret = buddy_free_unlocked(buddy, (u64)addr);
+ /* Drain any frees stranded during NMI/lock failure. */
+ if (unlikely(buddy_deferred_free_pending(buddy))) {
+ buddy_deferred_free_complete(buddy);
+ }
+
buddy_unlock(buddy, flags);
return ret;
}
/*
- * Fails only when the arguments are invalid or the buddy lock cannot be taken.
- * Nothing is freed in either case. Objects that buddy_free_unlocked() rejects
- * are reported there.
+ * Reject a NULL buddy or a NULL address array for a nonempty batch with -EINVAL.
+ * Valid objects are freed under the buddy lock or deferred on lock failure.
+ * Objects that buddy_free_unlocked() rejects are reported there.
*/
__weak int buddy_free_bulk(struct buddy __arena *buddy, void __arena * __arena *addrs, size_t naddrs)
{
@@ -1235,8 +1347,9 @@ __weak int buddy_free_bulk(struct buddy __arena *buddy, void __arena * __arena *
return -EINVAL;
ret = buddy_lock(buddy, flags);
- if (ret)
- return ret;
+ if (ret) {
+ return buddy_deferred_free_start_bulk(buddy, addrs, naddrs);
+ }
for (i = zero; i < naddrs && can_loop; i++) {
/* NULL pointers are ignored. */
@@ -1246,6 +1359,10 @@ __weak int buddy_free_bulk(struct buddy __arena *buddy, void __arena * __arena *
buddy_free_unlocked(buddy, (u64)addrs[i]);
}
+ /* Drain any frees stranded during an NMI/lock failure. */
+ if (unlikely(buddy_deferred_free_pending(buddy)))
+ buddy_deferred_free_complete(buddy);
+
buddy_unlock(buddy, flags);
return 0;
--
2.52.0
^ permalink raw reply related [flat|nested] 15+ messages in thread
* [PATCH bpf-next v5 7/9] selftests/bpf: libarena: Refill local caches in batches
2026-11-10 0:38 [PATCH bpf-next v5 0/9] bpf libarena allocator improvements Emil Tsalapatis
` (5 preceding siblings ...)
2026-11-10 0:38 ` [PATCH bpf-next v5 6/9] selftests/bpf: libarena: Handle NMI reentrancy Emil Tsalapatis
@ 2026-11-10 0:38 ` Emil Tsalapatis
2026-11-10 4:51 ` bot+bpf-ci
2026-11-10 0:38 ` [PATCH bpf-next v5 8/9] selftests/bpf: libarena: Add a transfer cache Emil Tsalapatis
` (2 subsequent siblings)
9 siblings, 1 reply; 15+ messages in thread
From: Emil Tsalapatis @ 2026-11-10 0:38 UTC (permalink / raw)
To: bpf; +Cc: ast, andrii, eddyz87, memxor, daniel, Emil Tsalapatis
Cache misses and full-cache frees take the buddy lock for each object.
Batch cache refills and spills to amortize locking and metadata work.
Refill without growing the allocator, falling back to individual
allocation when a batch is unavailable.
Signed-off-by: Emil Tsalapatis <emil@etsalapatis.com>
---
.../bpf/libarena/include/libarena/buddy.h | 6 +-
.../selftests/bpf/libarena/src/buddy.bpf.c | 56 +++++++++++++++++--
2 files changed, 57 insertions(+), 5 deletions(-)
diff --git a/tools/testing/selftests/bpf/libarena/include/libarena/buddy.h b/tools/testing/selftests/bpf/libarena/include/libarena/buddy.h
index 7553ad5a879d..a4d96909f5b0 100644
--- a/tools/testing/selftests/bpf/libarena/include/libarena/buddy.h
+++ b/tools/testing/selftests/bpf/libarena/include/libarena/buddy.h
@@ -81,10 +81,14 @@ enum buddy_cache_consts {
*/
BUDDY_CACHE_MAX_CPUS = 8192,
+ /* Objects moved to and from the buddy allocator at once, as an order. */
+ BUDDY_CACHE_BATCH_ORDER = 3,
+ BUDDY_CACHE_BATCH_SIZE = 1 << BUDDY_CACHE_BATCH_ORDER,
+
/*
* Objects retained locally for each allocation order.
*/
- BUDDY_CACHE_CAPACITY = 16,
+ BUDDY_CACHE_CAPACITY = BUDDY_CACHE_BATCH_SIZE * 2,
};
struct buddy_cache_order {
diff --git a/tools/testing/selftests/bpf/libarena/src/buddy.bpf.c b/tools/testing/selftests/bpf/libarena/src/buddy.bpf.c
index 7fd74f4c5052..23be400f0eaa 100644
--- a/tools/testing/selftests/bpf/libarena/src/buddy.bpf.c
+++ b/tools/testing/selftests/bpf/libarena/src/buddy.bpf.c
@@ -128,10 +128,28 @@ static __always_inline int buddy_cache_push(struct buddy_cache __arena *cache,
return 0;
}
-static void __arena *buddy_cache_alloc(struct buddy __arena *buddy, size_t order)
+/*
+ * 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.
+ */
+static __noinline void __arena *
+buddy_cache_refill(struct buddy __arena *buddy, struct buddy_cache __arena *cache,
+ size_t size, int order)
+{
+ struct buddy_cache_order __arena *slots = &cache->orders[order];
+
+ if (buddy_alloc_bulk_nogrow(buddy, size, BUDDY_CACHE_BATCH_ORDER,
+ (void __arena * __arena *)slots->objects))
+ return NULL;
+
+ WRITE_ONCE(slots->elems, BUDDY_CACHE_BATCH_SIZE - 1);
+ return slots->objects[BUDDY_CACHE_BATCH_SIZE - 1];
+}
+
+static void __arena *buddy_cache_alloc(struct buddy __arena *buddy, size_t size, size_t order)
{
struct buddy_cache __arena *cache;
- void __arena *address = NULL;
+ void __arena *address;
if (!buddy_order_cached(order))
return NULL;
@@ -140,7 +158,11 @@ static void __arena *buddy_cache_alloc(struct buddy __arena *buddy, size_t order
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())
+ address = buddy_cache_refill(buddy, cache, size, order);
+
buddy_cache_exit(cache);
return address;
@@ -171,6 +193,30 @@ static int buddy_cache_get_order(void __arena *addr)
return idx_get_order(chunk, idx);
}
+/*
+ * 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.
+ */
+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;
+
+ 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);
+}
+
static __always_inline int buddy_cache_free(struct buddy __arena *buddy, void __arena *addr)
{
struct buddy_cache __arena *cache;
@@ -190,6 +236,8 @@ static __always_inline int buddy_cache_free(struct buddy __arena *buddy, void __
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;
@@ -1030,7 +1078,7 @@ void __arena *buddy_alloc(struct buddy __arena *buddy, size_t size)
return NULL;
order = size_to_order(size);
- address = buddy_cache_alloc(buddy, order);
+ address = buddy_cache_alloc(buddy, size, order);
if (address)
return address;
@@ -1047,7 +1095,7 @@ void __arena *buddy_alloc_nogrow(struct buddy __arena *buddy, size_t size)
return NULL;
order = size_to_order(size);
- address = buddy_cache_alloc(buddy, order);
+ address = buddy_cache_alloc(buddy, size, order);
if (address)
return address;
--
2.52.0
^ permalink raw reply related [flat|nested] 15+ messages in thread
* [PATCH bpf-next v5 8/9] selftests/bpf: libarena: Add a transfer cache
2026-11-10 0:38 [PATCH bpf-next v5 0/9] bpf libarena allocator improvements Emil Tsalapatis
` (6 preceding siblings ...)
2026-11-10 0:38 ` [PATCH bpf-next v5 7/9] selftests/bpf: libarena: Refill local caches in batches Emil Tsalapatis
@ 2026-11-10 0:38 ` Emil Tsalapatis
2026-11-10 0:38 ` [PATCH bpf-next v5 9/9] selftests/bpf: libarena: Cover buddy cache fast paths Emil Tsalapatis
2026-11-12 21:00 ` [PATCH bpf-next v5 0/9] bpf libarena allocator improvements patchwork-bot+netdevbpf
9 siblings, 0 replies; 15+ messages in thread
From: Emil Tsalapatis @ 2026-11-10 0:38 UTC (permalink / raw)
To: bpf; +Cc: ast, andrii, eddyz87, memxor, daniel, Emil Tsalapatis
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 23be400f0eaa..467b8eb92d2a 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 @@ static void __arena *buddy_cache_alloc(struct buddy __arena *buddy, size_t size,
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
^ permalink raw reply related [flat|nested] 15+ messages in thread
* [PATCH bpf-next v5 9/9] selftests/bpf: libarena: Cover buddy cache fast paths
2026-11-10 0:38 [PATCH bpf-next v5 0/9] bpf libarena allocator improvements Emil Tsalapatis
` (7 preceding siblings ...)
2026-11-10 0:38 ` [PATCH bpf-next v5 8/9] selftests/bpf: libarena: Add a transfer cache Emil Tsalapatis
@ 2026-11-10 0:38 ` Emil Tsalapatis
2026-11-12 21:00 ` [PATCH bpf-next v5 0/9] bpf libarena allocator improvements patchwork-bot+netdevbpf
9 siblings, 0 replies; 15+ messages in thread
From: Emil Tsalapatis @ 2026-11-10 0:38 UTC (permalink / raw)
To: bpf; +Cc: ast, andrii, eddyz87, memxor, daniel, Emil Tsalapatis
Add coverage for the bulk allocation helpers. Check object stride, bounds
and contents, then free the objects in a non-sequential order to exercise
their independent metadata and coalescing.
Exercise local-cache initialization, batch refill, LIFO reuse and capacity
overflow. Verify that overflow moves a batch into the transfer cache and
that a later underflow consumes it without reaching the buddy allocator.
Document and test that explicit bulk allocations bypass the per-CPU cache.
Signed-off-by: Emil Tsalapatis <emil@etsalapatis.com>
---
.../bpf/libarena/selftests/test_buddy.bpf.c | 254 ++++++++++++++++++
.../selftests/bpf/libarena/src/buddy.bpf.c | 5 +
2 files changed, 259 insertions(+)
diff --git a/tools/testing/selftests/bpf/libarena/selftests/test_buddy.bpf.c b/tools/testing/selftests/bpf/libarena/selftests/test_buddy.bpf.c
index e7c6ac76433c..886a09019566 100644
--- a/tools/testing/selftests/bpf/libarena/selftests/test_buddy.bpf.c
+++ b/tools/testing/selftests/bpf/libarena/selftests/test_buddy.bpf.c
@@ -17,6 +17,24 @@ struct segarr_entry {
#define SEGARRLEN (512)
static struct segarr_entry __arena segarr[SEGARRLEN];
static void __arena *ptrs[17];
+
+enum buddy_alloc_path_test_consts {
+ TEST_BULK_NORDER = 3,
+ TEST_BULK_ITEMS = 1 << TEST_BULK_NORDER,
+ TEST_BULK_SIZE = 17,
+ TEST_BULK_STEP = 32,
+ TEST_CACHE_ORDER = 2,
+ TEST_CACHE_SIZE = BUDDY_MIN_ALLOC_BYTES << TEST_CACHE_ORDER,
+};
+
+struct buddy_alloc_path_test_storage {
+ void __arena *bulk[BUDDY_CACHE_CAPACITY + 1];
+ void __arena *to_free[BUDDY_CACHE_CAPACITY + 1];
+ void __arena *drain[BUDDY_CACHE_BATCH_SIZE];
+};
+
+static struct buddy_alloc_path_test_storage __arena alloc_path_test;
+
size_t __arena alloc_sizes[] = { 3, 17, 1025, 129, 16350, 333, 9, 517 };
size_t __arena alloc_multiple_sizes[] = { 3, 17, 1025, 129, 16350, 333, 9, 517, 2099 };
size_t __arena alloc_free_sizes[] = { 3, 17, 64, 129, 256, 333, 512, 517 };
@@ -221,4 +239,240 @@ __weak int test_buddy_alignment(void)
return 0;
}
+SEC("syscall")
+__weak int test_buddy_bulk_alloc_free(void)
+{
+ const size_t uncached_size =
+ (BUDDY_MIN_ALLOC_BYTES << (BUDDY_CACHE_NUM_ORDERS - 1)) + 1;
+ void __arena *sentinel = (void __arena *)&alloc_path_test;
+ u8 __arena *mem;
+ unsigned int i, j, idx;
+ int ret, destroy_ret;
+
+ ret = buddy_init(&buddy);
+ if (ret)
+ return ret;
+
+ /*
+ * A 17-byte request rounds up to a 32-byte buddy allocation. Verify
+ * that the bulk allocator returns every child at the expected stride,
+ * without writing beyond the requested output array.
+ */
+ alloc_path_test.bulk[TEST_BULK_ITEMS] = sentinel;
+ ret = buddy_alloc_bulk(&buddy, TEST_BULK_SIZE, TEST_BULK_NORDER,
+ alloc_path_test.bulk);
+ if (ret)
+ goto out;
+ if (alloc_path_test.bulk[TEST_BULK_ITEMS] != sentinel) {
+ ret = -EOVERFLOW;
+ goto out;
+ }
+
+ for (i = zero; i < TEST_BULK_ITEMS && can_loop; i++) {
+ mem = alloc_path_test.bulk[i];
+ if (!mem) {
+ ret = -ENOMEM;
+ goto out;
+ }
+ if (i && (u64)mem !=
+ (u64)alloc_path_test.bulk[i - 1] + TEST_BULK_STEP) {
+ ret = -EINVAL;
+ goto out;
+ }
+
+ for (j = zero; j < TEST_BULK_SIZE && can_loop; j++)
+ mem[j] = (u8)(i + 1);
+ }
+
+ for (i = zero; i < TEST_BULK_ITEMS && can_loop; i++) {
+ mem = alloc_path_test.bulk[i];
+ for (j = zero; j < TEST_BULK_SIZE && can_loop; j++) {
+ if (mem[j] != (u8)(i + 1)) {
+ ret = -EINVAL;
+ goto out;
+ }
+ }
+
+ /* Five is coprime to eight, producing a permutation of all items. */
+ idx = (i * 5) & (TEST_BULK_ITEMS - 1);
+ alloc_path_test.to_free[i] = alloc_path_test.bulk[idx];
+ }
+ alloc_path_test.to_free[TEST_BULK_ITEMS] = NULL;
+
+ ret = buddy_free_bulk(&buddy, alloc_path_test.to_free,
+ TEST_BULK_ITEMS + 1);
+ if (ret)
+ goto out;
+
+ /*
+ * Order eight is immediately above the per-CPU cache range. Free a bulk
+ * allocation one child at a time in a non-sequential order to exercise
+ * independent metadata and coalescing without the cache intercepting it.
+ */
+ ret = buddy_alloc_bulk(&buddy, uncached_size, TEST_BULK_NORDER,
+ alloc_path_test.bulk);
+ if (ret)
+ goto out;
+
+ for (i = zero; i < TEST_BULK_ITEMS && can_loop; i++) {
+ idx = (i * 5) & (TEST_BULK_ITEMS - 1);
+ ret = buddy_free(&buddy, alloc_path_test.bulk[idx]);
+ if (ret)
+ goto out;
+ }
+
+out:
+ destroy_ret = buddy_destroy(&buddy);
+ return ret ?: destroy_ret;
+}
+
+SEC("syscall")
+__weak int test_buddy_cache_paths(void)
+{
+#ifdef BPF_ARENA_ASAN
+ return 0;
+#else
+ struct buddy_cache __arena *cache;
+ struct buddy_cache_order __arena *slots;
+ struct buddy_transfer_order __arena *transfer;
+ void __arena *direct, *cached, *reused, *refilled, *overflow_reused;
+ void __arena *transferred;
+ unsigned int cpu, i;
+ int ret, destroy_ret;
+
+ ret = buddy_init(&buddy);
+ if (ret)
+ return ret;
+
+ cpu = bpf_get_smp_processor_id();
+ cache = buddy.percpu[cpu];
+ if (!cache) {
+ ret = -EINVAL;
+ goto out;
+ }
+ slots = &cache->orders[TEST_CACHE_ORDER];
+ transfer = &buddy.transfer[TEST_CACHE_ORDER];
+ if (READ_ONCE(slots->elems) || READ_ONCE(transfer->elems)) {
+ ret = -EINVAL;
+ goto out;
+ }
+
+ /* A miss bulk-refills the local order and returns one object directly. */
+ direct = buddy_alloc(&buddy, TEST_CACHE_SIZE);
+ if (!direct || READ_ONCE(slots->elems) != BUDDY_CACHE_BATCH_SIZE - 1) {
+ ret = -ENOMEM;
+ goto out;
+ }
+
+ /* Subsequent allocations and frees stay on the packed local stack. */
+ cached = buddy_alloc(&buddy, TEST_CACHE_SIZE);
+ if (!cached || READ_ONCE(slots->elems) != BUDDY_CACHE_BATCH_SIZE - 2) {
+ ret = -EINVAL;
+ goto out;
+ }
+
+ ret = buddy_free(&buddy, cached);
+ if (ret)
+ goto out;
+ reused = buddy_alloc(&buddy, TEST_CACHE_SIZE);
+ if (reused != cached) {
+ ret = -EINVAL;
+ goto out;
+ }
+
+ /* Drain the local stack, then verify that the next miss refills it. */
+ for (i = zero; i < BUDDY_CACHE_BATCH_SIZE - 2 && can_loop; i++) {
+ alloc_path_test.drain[i] = buddy_alloc(&buddy, TEST_CACHE_SIZE);
+ if (!alloc_path_test.drain[i]) {
+ ret = -ENOMEM;
+ goto out;
+ }
+ }
+ if (READ_ONCE(slots->elems)) {
+ ret = -EINVAL;
+ goto out;
+ }
+
+ refilled = buddy_alloc(&buddy, TEST_CACHE_SIZE);
+ if (!refilled ||
+ READ_ONCE(slots->elems) != BUDDY_CACHE_BATCH_SIZE - 1) {
+ ret = -EINVAL;
+ goto out;
+ }
+
+ /* Bulk allocation bypasses the cache. Fill the remaining local slots. */
+ ret = buddy_alloc_bulk(&buddy, TEST_CACHE_SIZE,
+ arena_fls(BUDDY_CACHE_CAPACITY) - 1,
+ alloc_path_test.bulk);
+ if (ret)
+ goto out;
+
+ for (i = zero;
+ i <= BUDDY_CACHE_CAPACITY - BUDDY_CACHE_BATCH_SIZE && can_loop;
+ i++) {
+ ret = buddy_free(&buddy, alloc_path_test.bulk[i]);
+ if (ret)
+ goto out;
+ }
+ if (READ_ONCE(slots->elems) != BUDDY_CACHE_CAPACITY) {
+ ret = -EINVAL;
+ goto out;
+ }
+
+ /* A free beyond local capacity transfers a batch to the shared cache. */
+ ret = buddy_free(&buddy,
+ alloc_path_test.bulk[BUDDY_CACHE_CAPACITY -
+ BUDDY_CACHE_BATCH_SIZE + 1]);
+ if (ret)
+ goto out;
+ if (READ_ONCE(slots->elems) !=
+ BUDDY_CACHE_CAPACITY - BUDDY_CACHE_BATCH_SIZE + 1 ||
+ READ_ONCE(transfer->elems) != BUDDY_CACHE_BATCH_SIZE) {
+ ret = -EINVAL;
+ goto out;
+ }
+
+ overflow_reused = buddy_alloc(&buddy, TEST_CACHE_SIZE);
+ if (overflow_reused != alloc_path_test.bulk[BUDDY_CACHE_CAPACITY -
+ BUDDY_CACHE_BATCH_SIZE + 1] ||
+ READ_ONCE(slots->elems) !=
+ BUDDY_CACHE_CAPACITY - BUDDY_CACHE_BATCH_SIZE) {
+ ret = -EINVAL;
+ goto out;
+ }
+
+ /* Drain locally, then consume the batch without touching buddy. */
+ for (i = zero;
+ i < BUDDY_CACHE_CAPACITY - BUDDY_CACHE_BATCH_SIZE && can_loop;
+ i++) {
+ alloc_path_test.drain[i] = buddy_alloc(&buddy, TEST_CACHE_SIZE);
+ if (!alloc_path_test.drain[i]) {
+ ret = -ENOMEM;
+ goto out;
+ }
+ }
+ transferred = buddy_alloc(&buddy, TEST_CACHE_SIZE);
+ if (transferred != alloc_path_test.bulk[BUDDY_CACHE_CAPACITY -
+ BUDDY_CACHE_BATCH_SIZE] ||
+ READ_ONCE(slots->elems) != BUDDY_CACHE_BATCH_SIZE - 1 ||
+ READ_ONCE(transfer->elems)) {
+ ret = -EINVAL;
+ goto out;
+ }
+
+ /* Interior pointers are rejected without consuming the allocation. */
+ i = BUDDY_CACHE_CAPACITY - BUDDY_CACHE_BATCH_SIZE + 2;
+ if (!buddy_free(&buddy, (u8 __arena *)alloc_path_test.bulk[i] +
+ BUDDY_MIN_ALLOC_BYTES)) {
+ ret = -EINVAL;
+ goto out;
+ }
+ ret = buddy_free(&buddy, alloc_path_test.bulk[i]);
+
+out:
+ destroy_ret = buddy_destroy(&buddy);
+ return ret ?: destroy_ret;
+#endif
+}
+
__weak char _license[] SEC("license") = "GPL";
diff --git a/tools/testing/selftests/bpf/libarena/src/buddy.bpf.c b/tools/testing/selftests/bpf/libarena/src/buddy.bpf.c
index 467b8eb92d2a..6e9992e3442b 100644
--- a/tools/testing/selftests/bpf/libarena/src/buddy.bpf.c
+++ b/tools/testing/selftests/bpf/libarena/src/buddy.bpf.c
@@ -1234,6 +1234,11 @@ static int buddy_alloc_bulk_memory(struct buddy __arena *buddy, size_t size, siz
if (!buddy || !out || norder >= BUDDY_CHUNK_NUM_ORDERS)
return -EINVAL;
+ /*
+ * Bulk allocations bypass the per-cpu caches and reserve one buddy
+ * block covering all returned objects. The buddy metadata records
+ * each object separately so it can later be freed independently.
+ */
order = size_to_order(size);
address = buddy_alloc_memory(buddy, size, order, norder, grow);
if (!address)
--
2.52.0
^ permalink raw reply related [flat|nested] 15+ messages in thread
* Re: [PATCH bpf-next v5 6/9] selftests/bpf: libarena: Handle NMI reentrancy
2026-11-10 0:38 ` [PATCH bpf-next v5 6/9] selftests/bpf: libarena: Handle NMI reentrancy Emil Tsalapatis
@ 2026-11-10 1:34 ` sashiko-bot
2026-11-10 4:51 ` bot+bpf-ci
1 sibling, 0 replies; 15+ messages in thread
From: sashiko-bot @ 2026-11-10 1:34 UTC (permalink / raw)
To: Emil Tsalapatis, bpf
Thank you for your contribution! Sashiko AI review found 1 potential
issue to consider:
- [Low] selftests/bpf: inconsistent error return in
buddy_deferred_free_start_bulk()
--
commit 6751ae768506dfa7101c71c63c2aa51a0d3eb6cc
Author: Emil Tsalapatis <emil@etsalapatis.com>
selftests/bpf: libarena: Handle NMI reentrancy
This commit handles NMI reentrancy in the libarena buddy allocator by using
a trylock in NMI context and deferring frees onto an intrusive lockless list
when the buddy lock cannot be acquired.
> diff --git a/tools/testing/selftests/bpf/libarena/src/buddy.bpf.c b/tools/testing/selftests/bpf/libarena/src/buddy.bpf.c
> index 58e666210fdbd..7fd74f4c50526 100644
> --- a/tools/testing/selftests/bpf/libarena/src/buddy.bpf.c
> +++ b/tools/testing/selftests/bpf/libarena/src/buddy.bpf.c
[ ... ]
> @@ -1187,6 +1201,98 @@ static __always_inline int buddy_free_unlocked(struct buddy __arena *buddy, u64
> return 0;
> }
>
> +static bool buddy_deferred_free_pending(struct buddy __arena *buddy)
> +{
> + return buddy->deferred_free != NULL;
> +}
[ ... ]
> +static int buddy_deferred_free_start_bulk(struct buddy __arena *buddy,
> + void __arena * __arena *addrs, size_t naddrs)
> +{
> + void __arena *first, *last, *addr;
> + unsigned int i;
> + int ret = 0, order;
> +
> + /*
> + * Chain the allocations into a local linked list. Possible because
> + * allocations are 16 bytes minimum and so fit a pointer.
> + */
> + first = NULL;
> + last = NULL;
> + for (i = zero; i < naddrs && can_loop; i++) {
> + addr = addrs[i];
> + if (!addr)
> + continue;
> +
> + order = buddy_cache_get_order(addr);
> + if (order >= BUDDY_CHUNK_NUM_ORDERS) {
> + ret = -EINVAL;
> + continue;
> + }
> +
> + asan_poison(addr, BUDDY_POISONED, BUDDY_MIN_ALLOC_BYTES << order);
> + asan_unpoison(addr, sizeof(void *));
> +
> + if (!first)
> + first = addr;
> + else
> + *(void __arena * __arena *)last = addr;
> +
> + last = addr;
> + }
> +
> + if (!first)
> + return ret;
> +
> + /* Splice the local list onto the deferred-free list. */
> + buddy_deferred_free_push(buddy, first, last);
> + return 0;
> +}
[Severity: Low]
Is the return value of buddy_deferred_free_start_bulk() intended when an
address in addrs[] has an invalid order?
When a batch contains both an invalid address and at least one valid
address, the final "return 0" discards the "ret = -EINVAL" recorded in the
loop. Conversely, when every non-NULL address in the batch is invalid,
"!first" is true and this returns -EINVAL, whereas the locked path in
buddy_free_bulk() returns 0 and leaves invalid-object reporting to
buddy_free_unlocked() (which is also skipped for addresses filtered out
here).
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261110003838.8739-1-emil@etsalapatis.com?part=6
^ permalink raw reply [flat|nested] 15+ messages in thread
* Re: [PATCH bpf-next v5 2/9] selftests/bpf: Add a parallel buddy allocator benchmark
2026-11-10 0:38 ` [PATCH bpf-next v5 2/9] selftests/bpf: Add a parallel buddy allocator benchmark Emil Tsalapatis
@ 2026-11-10 4:51 ` bot+bpf-ci
0 siblings, 0 replies; 15+ messages in thread
From: bot+bpf-ci @ 2026-11-10 4:51 UTC (permalink / raw)
To: emil, bpf
Cc: ast, andrii, eddyz87, memxor, daniel, emil, ast, andrii, daniel,
martin.lau, eddyz87, yonghong.song, mason, ihor.solodrai
[-- Attachment #1: Type: text/plain, Size: 1858 bytes --]
> selftests/bpf: Add a parallel buddy allocator benchmark
TL;DR: Nit only: the cross-CPU benchmark's cmpxchg() retry loop could be a
single atomic exchange, and its "free our own list" fallback comment seems
wrong, since can_loop expiry would skip the free.
> diff --git a/tools/testing/selftests/bpf/libarena/benchs/bench_malloc.bpf.c b/tools/testing/selftests/bpf/libarena/benchs/bench_malloc.bpf.c
> --- a/tools/testing/selftests/bpf/libarena/benchs/bench_malloc.bpf.c
> +++ b/tools/testing/selftests/bpf/libarena/benchs/bench_malloc.bpf.c
[ ... ]
> + /*
> + * Publish this list and take the one left by the previous invocation,
> + * usually allocated by a worker on another CPU. If the exchange does
> + * not complete, free our own list instead.
> + */
> + old = READ_ONCE(bench_xfree_head);
> + while (can_loop) {
> + cur = cmpxchg(&bench_xfree_head, old, head);
> + if (cur == old) {
> + head = old;
> + break;
> + }
> + old = cur;
> + }
> +
> + for (i = zero; head && can_loop; i++) {
This isn't a bug, but could this cmpxchg() retry loop be a single atomic
exchange, for example:
head = __atomic_exchange_n(&bench_xfree_head, head, __ATOMIC_SEQ_CST);
The new value of bench_xfree_head does not depend on the old one, so the
loop always ends up storing head.
That would also drop the fallback path. The comment says "free our own list
instead" if the exchange does not complete, but the loop only exits without
swapping once can_loop has expired. At that point the following free loop,
"head && can_loop", is already false on its first check, so the list would
not be freed anyway.
---
AI reviewed your patch. Please fix the bug or email reply why it's not a bug.
See: https://github.com/kernel-patches/vmtest/blob/master/ci/claude/README.md
CI run summary: https://github.com/kernel-patches/bpf/actions/runs/42755638928
^ permalink raw reply [flat|nested] 15+ messages in thread
* Re: [PATCH bpf-next v5 7/9] selftests/bpf: libarena: Refill local caches in batches
2026-11-10 0:38 ` [PATCH bpf-next v5 7/9] selftests/bpf: libarena: Refill local caches in batches Emil Tsalapatis
@ 2026-11-10 4:51 ` bot+bpf-ci
0 siblings, 0 replies; 15+ messages in thread
From: bot+bpf-ci @ 2026-11-10 4:51 UTC (permalink / raw)
To: emil, bpf
Cc: ast, andrii, eddyz87, memxor, daniel, emil, ast, andrii, daniel,
martin.lau, eddyz87, yonghong.song, mason, ihor.solodrai
[-- Attachment #1: Type: text/plain, Size: 2114 bytes --]
> selftests/bpf: libarena: Refill local caches in batches
TL;DR: Nit only: the new comment in buddy_cache_alloc() saying NMIs only use
cached objects looks inaccurate, since NMIs can still trylock the buddy lock
on miss or spill; the commit message omits the NMI exclusion.
> diff --git a/tools/testing/selftests/bpf/libarena/src/buddy.bpf.c b/tools/testing/selftests/bpf/libarena/src/buddy.bpf.c
[ ... ]
> @@ -140,7 +158,11 @@ static void __arena *buddy_cache_alloc(struct buddy __arena *buddy, size_t size, size_t order)
> 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())
> + address = buddy_cache_refill(buddy, cache, size, order);
> +
> buddy_cache_exit(cache);
This isn't a bug, but is this comment accurate? buddy_lock() uses a trylock
in NMI context rather than refusing the lock:
#define buddy_lock(buddy, flags) \
(bpf_in_nmi() ? arena_spin_trylock_irqsave(&(buddy)->lock, (flags)) : \
arena_spin_lock_irqsave(&(buddy)->lock, (flags)))
On an NMI cache miss, buddy_cache_alloc() returns NULL and buddy_alloc()
goes on to buddy_alloc_memory(), which calls buddy_lock(). An NMI free into
a full cache also reaches the lock:
buddy_free() -> buddy_cache_free() -> buddy_cache_spill() ->
buddy_free_bulk() -> buddy_lock()
So NMIs do not only use what is already cached, and skipping the refill does
not keep NMIs off the buddy lock. Would something like "NMIs may only
trylock the buddy lock, so do not refill the cache from NMI context" describe
the intent better?
The commit message does not mention the NMI exclusion either. The later
commit 74ddd93cc764 ("selftests/bpf: libarena: Add a transfer cache") removes
this comment and the !bpf_in_nmi() check.
---
AI reviewed your patch. Please fix the bug or email reply why it's not a bug.
See: https://github.com/kernel-patches/vmtest/blob/master/ci/claude/README.md
CI run summary: https://github.com/kernel-patches/bpf/actions/runs/42755638928
^ permalink raw reply [flat|nested] 15+ messages in thread
* Re: [PATCH bpf-next v5 6/9] selftests/bpf: libarena: Handle NMI reentrancy
2026-11-10 0:38 ` [PATCH bpf-next v5 6/9] selftests/bpf: libarena: Handle NMI reentrancy Emil Tsalapatis
2026-11-10 1:34 ` sashiko-bot
@ 2026-11-10 4:51 ` bot+bpf-ci
1 sibling, 0 replies; 15+ messages in thread
From: bot+bpf-ci @ 2026-11-10 4:51 UTC (permalink / raw)
To: emil, bpf
Cc: ast, andrii, eddyz87, memxor, daniel, emil, ast, andrii, daniel,
martin.lau, eddyz87, yonghong.song, mason, ihor.solodrai
[-- Attachment #1: Type: text/plain, Size: 2370 bytes --]
> selftests/bpf: libarena: Handle NMI reentrancy
TL;DR:
- In a standalone libarena build, bpf_in_nmi() may always return false, so the
NMI trylock and guards would never fire
- New dependency on bpf_experimental.h breaks libarena self-containment, with
a possible license mismatch
> diff --git a/tools/testing/selftests/bpf/libarena/src/buddy.bpf.c b/tools/testing/selftests/bpf/libarena/src/buddy.bpf.c
> --- a/tools/testing/selftests/bpf/libarena/src/buddy.bpf.c
> +++ b/tools/testing/selftests/bpf/libarena/src/buddy.bpf.c
> @@ -6,6 +6,7 @@
> #include <libarena/buddy.h>
>
> #include <bpf_arena_spin_lock.h>
> +#include <bpf_experimental.h>
>
This is the first libarena source that pulls in a header living only in
tools/testing/selftests/bpf (found through -I$(BPFDIR)). Before this patch,
every libarena include came from libarena/include, and commit 8c1e1c33fe5a
("Move arena-related headers into libarena") moved bpf_may_goto.h out of
bpf_experimental.h to keep libarena self-contained.
Is the license also a concern here? bpf_experimental.h has no SPDX tag, so
it falls under the tree default GPL-2.0, while buddy.bpf.c is
LGPL-2.1 OR BSD-2-Clause, and the bpf_in_nmi()/get_preempt_count() code is
now inlined from it.
There also seems to be a functional problem. get_preempt_count() selects its
implementation with:
#if defined(bpf_target_x86)
...
and those macros come from <bpf/bpf_tracing.h>, which only sets them when
-D__TARGET_ARCH_* is passed. The selftests build passes it via BPF_CFLAGS,
but the "Defaults for standalone builds" in libarena/Makefile do not.
In a standalone "make -C libarena" build, can get_preempt_count() fall
through to "return 0;", making bpf_in_nmi() always false? Then buddy_lock()
would always use the waiting arena_spin_lock_irqsave(), and the
"!grow || bpf_in_nmi()" check in buddy_alloc_memory() and the NMI guard in
buddy_grow() would never fire.
Would it be better to move bpf_in_nmi(), get_preempt_count() and the
preempt-mask defines into a libarena header, as was done for bpf_may_goto.h,
and make sure a standalone build defines __TARGET_ARCH_*?
---
AI reviewed your patch. Please fix the bug or email reply why it's not a bug.
See: https://github.com/kernel-patches/vmtest/blob/master/ci/claude/README.md
CI run summary: https://github.com/kernel-patches/bpf/actions/runs/42755638928
^ permalink raw reply [flat|nested] 15+ messages in thread
* Re: [PATCH bpf-next v5 0/9] bpf libarena allocator improvements
2026-11-10 0:38 [PATCH bpf-next v5 0/9] bpf libarena allocator improvements Emil Tsalapatis
` (8 preceding siblings ...)
2026-11-10 0:38 ` [PATCH bpf-next v5 9/9] selftests/bpf: libarena: Cover buddy cache fast paths Emil Tsalapatis
@ 2026-11-12 21:00 ` patchwork-bot+netdevbpf
9 siblings, 0 replies; 15+ messages in thread
From: patchwork-bot+netdevbpf @ 2026-11-12 21:00 UTC (permalink / raw)
To: Emil Tsalapatis; +Cc: bpf, ast, andrii, eddyz87, memxor, daniel, harry
Hello:
This series was applied to bpf/bpf-next.git (master)
by Alexei Starovoitov <ast@kernel.org>:
On Tue, 10 Nov 2026 00:38:29 +0000 you wrote:
> The libarena buddy allocator's design has up until now prioritized
> ease of verification. With new verifier features like unlimited
> global stack frames and a large stack size we can now improve
> the allocator's performance and robustness.
>
> This patch introduces per-CPU caches for the allocator. The overall
> caching design is inspired by tcmalloc. Each CPU has its own static
> object LIFOs, one for each allocator order. This speeds up allocation
> and freeing for BPF programs that have some degree of locality. The
> CPU caches are lockless. The caches are replenished from and spilled
> into "transfer caches", per-order caches shared between the CPUs that
> amortize the cost of spilling/refilling. Data from the transfer caches
> is in turn moved from and to the allocator.
>
> [...]
Here is the summary with links:
- [bpf-next,v5,1/9] selftests/bpf: libarena: Add bulk buddy allocation helpers
https://git.kernel.org/bpf/bpf-next/c/d8276c0d08cc
- [bpf-next,v5,2/9] selftests/bpf: Add a parallel buddy allocator benchmark
https://git.kernel.org/bpf/bpf-next/c/4d71673bc4b5
- [bpf-next,v5,3/9] selftests/bpf: libarena: Make spinlock trylock match arena_spin_unlock
https://git.kernel.org/bpf/bpf-next/c/b77e7e9c49fd
- [bpf-next,v5,4/9] selftests/bpf: libarena: Raise arena spinlock CPU limits
https://git.kernel.org/bpf/bpf-next/c/27f2cf406a35
- [bpf-next,v5,5/9] selftests/bpf: libarena: Add buddy per-CPU caches
https://git.kernel.org/bpf/bpf-next/c/c8d21d25241b
- [bpf-next,v5,6/9] selftests/bpf: libarena: Handle NMI reentrancy
https://git.kernel.org/bpf/bpf-next/c/633635ab8d51
- [bpf-next,v5,7/9] selftests/bpf: libarena: Refill local caches in batches
https://git.kernel.org/bpf/bpf-next/c/e5728f923b03
- [bpf-next,v5,8/9] selftests/bpf: libarena: Add a transfer cache
https://git.kernel.org/bpf/bpf-next/c/be73f5c9f9d3
- [bpf-next,v5,9/9] selftests/bpf: libarena: Cover buddy cache fast paths
https://git.kernel.org/bpf/bpf-next/c/ed5558fb22a3
You are awesome, thank you!
--
Deet-doot-dot, I am a bot.
https://korg.docs.kernel.org/patchwork/pwbot.html
^ permalink raw reply [flat|nested] 15+ messages in thread
end of thread, other threads:[~2026-11-12 21:00 UTC | newest]
Thread overview: 15+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-11-10 0:38 [PATCH bpf-next v5 0/9] bpf libarena allocator improvements Emil Tsalapatis
2026-11-10 0:38 ` [PATCH bpf-next v5 1/9] selftests/bpf: libarena: Add bulk buddy allocation helpers Emil Tsalapatis
2026-11-10 0:38 ` [PATCH bpf-next v5 2/9] selftests/bpf: Add a parallel buddy allocator benchmark Emil Tsalapatis
2026-11-10 4:51 ` bot+bpf-ci
2026-11-10 0:38 ` [PATCH bpf-next v5 3/9] selftests/bpf: libarena: Make spinlock trylock match arena_spin_unlock Emil Tsalapatis
2026-11-10 0:38 ` [PATCH bpf-next v5 4/9] selftests/bpf: libarena: Raise arena spinlock CPU limits Emil Tsalapatis
2026-11-10 0:38 ` [PATCH bpf-next v5 5/9] selftests/bpf: libarena: Add buddy per-CPU caches Emil Tsalapatis
2026-11-10 0:38 ` [PATCH bpf-next v5 6/9] selftests/bpf: libarena: Handle NMI reentrancy Emil Tsalapatis
2026-11-10 1:34 ` sashiko-bot
2026-11-10 4:51 ` bot+bpf-ci
2026-11-10 0:38 ` [PATCH bpf-next v5 7/9] selftests/bpf: libarena: Refill local caches in batches Emil Tsalapatis
2026-11-10 4:51 ` bot+bpf-ci
2026-11-10 0:38 ` [PATCH bpf-next v5 8/9] selftests/bpf: libarena: Add a transfer cache Emil Tsalapatis
2026-11-10 0:38 ` [PATCH bpf-next v5 9/9] selftests/bpf: libarena: Cover buddy cache fast paths Emil Tsalapatis
2026-11-12 21:00 ` [PATCH bpf-next v5 0/9] bpf libarena allocator improvements patchwork-bot+netdevbpf
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox