* [PATCH bpf-next v2 0/7] bpf libarena allocator improvements
@ 2026-10-21 23:26 Emil Tsalapatis
2026-10-21 23:26 ` [PATCH bpf-next v2 1/7] selftests/bpf: libarena: Add bulk buddy allocation helpers Emil Tsalapatis
` (6 more replies)
0 siblings, 7 replies; 11+ messages in thread
From: Emil Tsalapatis @ 2026-10-21 23:26 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 260/64 4.1x 602/65 9.3x 866/65 13.3x 4067/68 59.8x
4 244/62 3.9x 449/63 7.1x 2264/67 33.8x 4813/75 64.2x
16 233/62 3.8x 1047/63 16.6x 2198/69 31.9x 5044/77 65.5x
64 232/75 3.1x 883/142 6.2x 2558/191 13.4x 5603/380 14.7x
256 232/104 2.2x 1288/149 8.6x 2758/240 11.5x 5549/446 12.4x
Free latency (before/after ns, speedup):
allocs 1 prod 2 prod 4 prod 8 prod
1 236/66 3.6x 250/67 3.7x 588/67 8.8x 4014/66 60.8x
4 236/61 3.9x 353/61 5.8x 2373/62 38.3x 4979/62 80.3x
16 231/60 3.9x 1104/61 18.1x 2233/61 36.6x 5126/61 84.0x
64 231/75 3.1x 861/97 8.9x 2555/140 18.2x 5666/331 17.1x
256 230/137 1.7x 1182/107 11.0x 2766/179 15.5x 5584/404 13.8x
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 273/64 4.3x 865/64 13.5x 1192/65 18.3x 4274/69 61.9x
4 231/62 3.7x 740/63 11.7x 2431/67 36.3x 5053/79 64.0x
16 221/62 3.6x 342/63 5.4x 2233/68 32.8x 4991/89 56.1x
64 240/90 2.7x 959/153 6.3x 1503/113 13.3x 5178/353 14.7x
256 221/102 2.2x 1294/210 6.2x 2512/252 10.0x 5640/374 15.1x
Free latency (before/after ns, speedup):
allocs 1 prod 2 prod 4 prod 8 prod
1 250/66 3.8x 594/66 9.0x 1055/65 16.2x 4147/66 62.8x
4 224/61 3.7x 590/62 9.5x 2393/61 39.2x 5061/62 81.6x
16 212/60 3.5x 304/61 5.0x 2158/62 34.8x 4984/61 81.7x
64 233/86 2.7x 915/105 8.7x 1469/95 15.5x 5174/268 19.3x
256 214/133 1.6x 1215/121 10.0x 2511/166 15.1x 5633/309 18.2x
Cc: Harry Yoo <harry@kernel.org>
Cc: Kumar Kartikeya Dwivedi <memxor@gmail.com>
Signed-off-by: Emil Tsalapatis <emil@etsalapatis.com>
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 (7):
selftests/bpf: libarena: Add bulk buddy allocation helpers
selftests/bpf: Add a parallel buddy allocator benchmark
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 +++
.../bpf/libarena/include/libarena/buddy.h | 56 ++
.../bpf/libarena/include/libarena/common.h | 4 +
.../bpf/libarena/selftests/test_buddy.bpf.c | 273 ++++++++
.../selftests/bpf/libarena/src/buddy.bpf.c | 630 +++++++++++++++++-
8 files changed, 1166 insertions(+), 45 deletions(-)
--
2.52.0
^ permalink raw reply [flat|nested] 11+ messages in thread
* [PATCH bpf-next v2 1/7] selftests/bpf: libarena: Add bulk buddy allocation helpers
2026-10-21 23:26 [PATCH bpf-next v2 0/7] bpf libarena allocator improvements Emil Tsalapatis
@ 2026-10-21 23:26 ` Emil Tsalapatis
2026-10-21 23:26 ` [PATCH bpf-next v2 2/7] selftests/bpf: Add a parallel buddy allocator benchmark Emil Tsalapatis
` (5 subsequent siblings)
6 siblings, 0 replies; 11+ messages in thread
From: Emil Tsalapatis @ 2026-10-21 23:26 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 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 | 4 +
.../selftests/bpf/libarena/src/buddy.bpf.c | 142 ++++++++++++++----
2 files changed, 118 insertions(+), 28 deletions(-)
diff --git a/tools/testing/selftests/bpf/libarena/include/libarena/buddy.h b/tools/testing/selftests/bpf/libarena/include/libarena/buddy.h
index 528c69a1f38e..f150e1c37e3d 100644
--- a/tools/testing/selftests/bpf/libarena/include/libarena/buddy.h
+++ b/tools/testing/selftests/bpf/libarena/include/libarena/buddy.h
@@ -76,6 +76,10 @@ 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);
+int buddy_alloc_bulk(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 2490ab1396de..6dfaa23089a9 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,33 +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;
}
-__weak
-void __arena *buddy_alloc(struct buddy __arena *buddy, size_t size)
+
+static void __arena *buddy_alloc_memory(struct buddy __arena *buddy, size_t size,
+ int order, size_t norder)
{
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;
@@ -775,28 +785,68 @@ void __arena *buddy_alloc(struct buddy __arena *buddy, size_t size)
/* 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;
+
+ 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(address, size);
+ asan_unpoison(object, size);
+ }
return address;
}
-static __always_inline int buddy_free_unlocked(struct buddy __arena *buddy, u64 addr)
+__weak
+int buddy_alloc_bulk(struct buddy __arena *buddy, size_t size, size_t norder,
+ void __arena * __arena *out)
+{
+ 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);
+ 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
+void __arena *buddy_alloc(struct buddy __arena *buddy, size_t size)
+{
+ int order;
+
+ order = size_to_order(size);
+ return buddy_alloc_memory(buddy, size, order, 0);
+}
+
+static int buddy_free_unlocked(struct buddy __arena *buddy, u64 addr)
{
struct buddy_header __arena *header, *buddy_header;
u64 idx, buddy_idx, tmp_idx;
@@ -900,4 +950,40 @@ __weak int buddy_free(struct buddy __arena *buddy, void __arena *addr)
return ret;
}
+/*
+ * Fails only when the buddy lock cannot be taken, in which case nothing was
+ * freed. 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;
+}
+
__weak char _license[] SEC("license") = "GPL";
--
2.52.0
^ permalink raw reply related [flat|nested] 11+ messages in thread
* [PATCH bpf-next v2 2/7] selftests/bpf: Add a parallel buddy allocator benchmark
2026-10-21 23:26 [PATCH bpf-next v2 0/7] bpf libarena allocator improvements Emil Tsalapatis
2026-10-21 23:26 ` [PATCH bpf-next v2 1/7] selftests/bpf: libarena: Add bulk buddy allocation helpers Emil Tsalapatis
@ 2026-10-21 23:26 ` Emil Tsalapatis
2026-10-21 23:26 ` [PATCH bpf-next v2 3/7] selftests/bpf: libarena: Add buddy per-CPU caches Emil Tsalapatis
` (4 subsequent siblings)
6 siblings, 0 replies; 11+ messages in thread
From: Emil Tsalapatis @ 2026-10-21 23:26 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 e06397dc9a40..dedecee6d80f 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)
@@ -49,3 +52,110 @@ int bench_calloc(void)
__sync_add_and_fetch(&bench_hits, i);
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;
+}
--
2.52.0
^ permalink raw reply related [flat|nested] 11+ messages in thread
* [PATCH bpf-next v2 3/7] selftests/bpf: libarena: Add buddy per-CPU caches
2026-10-21 23:26 [PATCH bpf-next v2 0/7] bpf libarena allocator improvements Emil Tsalapatis
2026-10-21 23:26 ` [PATCH bpf-next v2 1/7] selftests/bpf: libarena: Add bulk buddy allocation helpers Emil Tsalapatis
2026-10-21 23:26 ` [PATCH bpf-next v2 2/7] selftests/bpf: Add a parallel buddy allocator benchmark Emil Tsalapatis
@ 2026-10-21 23:26 ` Emil Tsalapatis
2026-10-22 12:53 ` Harry Yoo
2026-10-21 23:26 ` [PATCH bpf-next v2 4/7] selftests/bpf: libarena: Handle NMI reentrancy Emil Tsalapatis
` (3 subsequent siblings)
6 siblings, 1 reply; 11+ messages in thread
From: Emil Tsalapatis @ 2026-10-21 23:26 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, bounding each
CPU's cache to about 1.5 KiB.
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 | 215 +++++++++++++++++-
4 files changed, 257 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 f150e1c37e3d..66349915745b 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 = 1024,
+
+ /*
+ * 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 a6220355a697..a20947911d34 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 6dfaa23089a9..c41e163b014f 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);
+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;
+}
+
+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);
+}
+
+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.
@@ -840,9 +1033,17 @@ int buddy_alloc_bulk(struct buddy __arena *buddy, size_t size, size_t norder,
__weak
void __arena *buddy_alloc(struct buddy __arena *buddy, size_t size)
{
+ 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);
}
@@ -939,6 +1140,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] 11+ messages in thread
* [PATCH bpf-next v2 4/7] selftests/bpf: libarena: Handle NMI reentrancy
2026-10-21 23:26 [PATCH bpf-next v2 0/7] bpf libarena allocator improvements Emil Tsalapatis
` (2 preceding siblings ...)
2026-10-21 23:26 ` [PATCH bpf-next v2 3/7] selftests/bpf: libarena: Add buddy per-CPU caches Emil Tsalapatis
@ 2026-10-21 23:26 ` Emil Tsalapatis
2026-10-21 23:26 ` [PATCH bpf-next v2 5/7] selftests/bpf: libarena: Refill local caches in batches Emil Tsalapatis
` (2 subsequent siblings)
6 siblings, 0 replies; 11+ messages in thread
From: Emil Tsalapatis @ 2026-10-21 23:26 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 take that lock.
Allow an allocation from NMI context only when the local cache can satisfy
it without taking the buddy lock.
When an NMI free cannot use the local cache, link the object onto an
intrusive deferred-free list. A later allocator operation from a safe
context drains that list while holding the buddy lock.
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 | 129 +++++++++++++++++-
3 files changed, 133 insertions(+), 1 deletion(-)
diff --git a/tools/testing/selftests/bpf/libarena/include/libarena/buddy.h b/tools/testing/selftests/bpf/libarena/include/libarena/buddy.h
index 66349915745b..de8d29ea4673 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 c41e163b014f..e19104ad7422 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,6 +49,8 @@ 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);
static int buddy_free_unlocked(struct buddy __arena *buddy, u64 addr);
+static bool buddy_deferred_free_pending(struct buddy __arena *buddy);
+static int buddy_deferred_free_complete(struct buddy __arena *buddy);
#define buddy_lock(buddy, flags) (arena_spin_lock_irqsave(&(buddy)->lock, (flags)))
#define buddy_unlock(buddy, flags) (arena_spin_unlock_irqrestore(&(buddy)->lock, (flags)))
@@ -747,6 +750,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 +821,7 @@ __weak int buddy_destroy(struct buddy __arena *buddy)
/* Clear all fields. */
buddy->first_chunk = NULL;
+ buddy->deferred_free = NULL;
return 0;
}
@@ -967,9 +972,19 @@ static void __arena *buddy_alloc_memory(struct buddy __arena *buddy, size_t size
return NULL;
}
+ if (bpf_in_nmi())
+ return NULL;
+
if (buddy_lock(buddy, flags))
return NULL;
+ if (unlikely(buddy_deferred_free_pending(buddy))) {
+ if (buddy_deferred_free_complete(buddy)) {
+ buddy_unlock(buddy, flags);
+ return NULL;
+ }
+ }
+
address = (u8 __arena *)buddy_alloc_from_existing_chunks(buddy, order, norder);
buddy_unlock(buddy, flags);
if (address)
@@ -1128,10 +1143,102 @@ static int buddy_free_unlocked(struct buddy __arena *buddy, u64 addr)
return 0;
}
+static bool buddy_deferred_free_pending(struct buddy __arena *buddy)
+{
+ return buddy->deferred_free != NULL;
+}
+
+static void buddy_deferred_free_start(struct buddy __arena *buddy, void __arena *addr)
+{
+ void __arena *head;
+ void __arena * __arena *new;
+
+ /*
+ * Make a linked list by chaining the allocations into a linked list.
+ * Possible because allocations are 16 bytes minimum and so fit a pointer.
+ */
+ new = (void __arena * __arena *)addr;
+ asan_unpoison(addr, sizeof(void *));
+ do {
+ head = buddy->deferred_free;
+ *new = head;
+ } while (cmpxchg((void __arena * __arena *)&buddy->deferred_free,
+ head, (void __arena *)new) != head && can_loop);
+}
+
+static void buddy_deferred_free_start_bulk(struct buddy __arena *buddy,
+ void __arena * __arena *addrs, size_t naddrs)
+{
+ void __arena *head, *first, *last, *addr;
+ unsigned int i;
+
+ /*
+ * 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;
+
+ asan_unpoison(addr, sizeof(void *));
+
+ if (!first)
+ first = addr;
+ else
+ *(void __arena * __arena *)last = addr;
+
+ last = addr;
+ }
+
+ if (!first)
+ return;
+
+ /* Splice the local list onto the deferred-free list. */
+ do {
+ head = 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_complete(struct buddy __arena *buddy)
+{
+ void __arena *head, *old;
+ int ret = 0, err;
+
+ do {
+ head = buddy->deferred_free;
+ if (!head)
+ break;
+ old = head;
+
+ /*
+ * The cmpxchg is safe because we never pop from the list
+ * without holding the buddy lock. The next pointer of
+ * the element then cannot have been gotten invalidated.
+ */
+
+ } while (cmpxchg((void __arena * __arena *)&buddy->deferred_free,
+ old, NULL) != old && can_loop);
+
+ while (head && can_loop) {
+ old = head;
+ head = *(void __arena * __arena *)head;
+
+ err = buddy_free_unlocked(buddy, (u64)old);
+ ret = ret ?: err;
+ }
+
+ return ret;
+}
+
__weak int buddy_free(struct buddy __arena *buddy, void __arena *addr)
{
unsigned long flags;
- int ret;
+ int ret, err;
if (!buddy)
return -EINVAL;
@@ -1144,12 +1251,23 @@ __weak int buddy_free(struct buddy __arena *buddy, void __arena *addr)
if (!ret)
return 0;
+ if (bpf_in_nmi()) {
+ buddy_deferred_free_start(buddy, addr);
+ return 0;
+ }
+
ret = buddy_lock(buddy, flags);
if (ret)
return ret;
ret = buddy_free_unlocked(buddy, (u64)addr);
+ /* Drain any frees stranded during an NMI. */
+ if (unlikely(buddy_deferred_free_pending(buddy))) {
+ err = buddy_deferred_free_complete(buddy);
+ ret = ret ?: err;
+ }
+
buddy_unlock(buddy, flags);
return ret;
@@ -1174,6 +1292,11 @@ __weak int buddy_free_bulk(struct buddy __arena *buddy, void __arena * __arena *
if (!addrs)
return -EINVAL;
+ if (bpf_in_nmi()) {
+ buddy_deferred_free_start_bulk(buddy, addrs, naddrs);
+ return 0;
+ }
+
ret = buddy_lock(buddy, flags);
if (ret)
return ret;
@@ -1186,6 +1309,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. */
+ 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] 11+ messages in thread
* [PATCH bpf-next v2 5/7] selftests/bpf: libarena: Refill local caches in batches
2026-10-21 23:26 [PATCH bpf-next v2 0/7] bpf libarena allocator improvements Emil Tsalapatis
` (3 preceding siblings ...)
2026-10-21 23:26 ` [PATCH bpf-next v2 4/7] selftests/bpf: libarena: Handle NMI reentrancy Emil Tsalapatis
@ 2026-10-21 23:26 ` Emil Tsalapatis
2026-10-21 23:26 ` [PATCH bpf-next v2 6/7] selftests/bpf: libarena: Add a transfer cache Emil Tsalapatis
2026-10-21 23:26 ` [PATCH bpf-next v2 7/7] selftests/bpf: libarena: Cover buddy cache fast paths Emil Tsalapatis
6 siblings, 0 replies; 11+ messages in thread
From: Emil Tsalapatis @ 2026-10-21 23:26 UTC (permalink / raw)
To: bpf; +Cc: ast, andrii, eddyz87, memxor, daniel, Emil Tsalapatis
Cache hits avoid the central lock, but every cache miss still asks the
buddy allocator for one object. A sequence of misses therefore repeats
the same locking and metadata work.
Allocate eight objects with buddy_alloc_bulk() on a local-cache miss.
Return one object directly and retain the other seven in the per-CPU LIFO
array. This amortizes buddy locking and metadata updates over subsequent
allocations. Skip the refill in NMI context, where the backend allocator
cannot safely take its lock.
Express the array capacity as twice the batch size, so that a refilled
array can still absorb a full batch of frees before it overflows.
Signed-off-by: Emil Tsalapatis <emil@etsalapatis.com>
---
.../bpf/libarena/include/libarena/buddy.h | 6 +++-
.../selftests/bpf/libarena/src/buddy.bpf.c | 28 +++++++++++++++++--
2 files changed, 30 insertions(+), 4 deletions(-)
diff --git a/tools/testing/selftests/bpf/libarena/include/libarena/buddy.h b/tools/testing/selftests/bpf/libarena/include/libarena/buddy.h
index de8d29ea4673..0c984952042a 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 = 1024,
+ /* 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 e19104ad7422..d7c3defeb0a8 100644
--- a/tools/testing/selftests/bpf/libarena/src/buddy.bpf.c
+++ b/tools/testing/selftests/bpf/libarena/src/buddy.bpf.c
@@ -126,10 +126,28 @@ static __always_inline int buddy_cache_push(struct buddy_cache __arena *cache,
return 0;
}
-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(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];
+}
+
+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;
@@ -138,7 +156,11 @@ 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;
@@ -1055,7 +1077,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;
--
2.52.0
^ permalink raw reply related [flat|nested] 11+ messages in thread
* [PATCH bpf-next v2 6/7] selftests/bpf: libarena: Add a transfer cache
2026-10-21 23:26 [PATCH bpf-next v2 0/7] bpf libarena allocator improvements Emil Tsalapatis
` (4 preceding siblings ...)
2026-10-21 23:26 ` [PATCH bpf-next v2 5/7] selftests/bpf: libarena: Refill local caches in batches Emil Tsalapatis
@ 2026-10-21 23:26 ` Emil Tsalapatis
2026-10-21 23:26 ` [PATCH bpf-next v2 7/7] selftests/bpf: libarena: Cover buddy cache fast paths Emil Tsalapatis
6 siblings, 0 replies; 11+ messages in thread
From: Emil Tsalapatis @ 2026-10-21 23:26 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. NMI
context never takes the transfer lock and spills directly. Keep both
slow paths out of line so that cache hits do not pay for them.
Signed-off-by: Emil Tsalapatis <emil@etsalapatis.com>
---
.../bpf/libarena/include/libarena/buddy.h | 11 ++
.../selftests/bpf/libarena/src/buddy.bpf.c | 123 +++++++++++++++++-
2 files changed, 131 insertions(+), 3 deletions(-)
diff --git a/tools/testing/selftests/bpf/libarena/include/libarena/buddy.h b/tools/testing/selftests/bpf/libarena/include/libarena/buddy.h
index 0c984952042a..1c62ae6e3fd0 100644
--- a/tools/testing/selftests/bpf/libarena/include/libarena/buddy.h
+++ b/tools/testing/selftests/bpf/libarena/include/libarena/buddy.h
@@ -89,6 +89,9 @@ enum buddy_cache_consts {
* Objects retained locally for each allocation order.
*/
BUDDY_CACHE_CAPACITY = BUDDY_CACHE_BATCH_SIZE * 2,
+
+ /* Number of objects shared between per-CPU caches for each order. */
+ BUDDY_TRANSFER_CAPACITY = BUDDY_CACHE_BATCH_SIZE * 8,
};
struct buddy_cache_order {
@@ -104,11 +107,19 @@ struct buddy_cache {
struct buddy_cache_order orders[BUDDY_CACHE_NUM_ORDERS];
};
+/* Per-order array shared between the per-CPU caches. */
+struct buddy_transfer_order {
+ arena_spinlock_t lock;
+ u32 elems;
+ void __arena *objects[BUDDY_TRANSFER_CAPACITY];
+} __attribute__((aligned(64)));
+
struct buddy {
struct buddy_chunk __arena *first_chunk; /* Pointer to the chunk linked list. */
arena_spinlock_t lock; /* Allocator lock */
u64 vaddr; /* Allocation into reserved vaddr */
struct buddy_cache __arena *percpu[BUDDY_CACHE_MAX_CPUS];
+ struct buddy_transfer_order transfer[BUDDY_CACHE_NUM_ORDERS];
struct buddy_deferred_free __arena *deferred_free;
};
diff --git a/tools/testing/selftests/bpf/libarena/src/buddy.bpf.c b/tools/testing/selftests/bpf/libarena/src/buddy.bpf.c
index d7c3defeb0a8..80f2a49f95e5 100644
--- a/tools/testing/selftests/bpf/libarena/src/buddy.bpf.c
+++ b/tools/testing/selftests/bpf/libarena/src/buddy.bpf.c
@@ -126,9 +126,78 @@ static __always_inline int buddy_cache_push(struct buddy_cache __arena *cache,
return 0;
}
+static int buddy_transfer_take_batch(struct buddy __arena *buddy, int order,
+ struct buddy_cache_order __arena *slots)
+{
+ struct buddy_transfer_order __arena *transfer = &buddy->transfer[order];
+ unsigned long flags;
+ u32 elems, start;
+ unsigned int i;
+ int ret = 0;
+
+ if (arena_spin_lock_irqsave(&transfer->lock, flags))
+ return -EAGAIN;
+
+ elems = READ_ONCE(transfer->elems);
+ if (elems < BUDDY_CACHE_BATCH_SIZE) {
+ ret = -ENOENT;
+ goto out_unlock;
+ }
+
+ start = elems - BUDDY_CACHE_BATCH_SIZE;
+ for (i = zero; i < BUDDY_CACHE_BATCH_SIZE && can_loop; i++)
+ WRITE_ONCE(slots->objects[i],
+ READ_ONCE(transfer->objects[start + i]));
+
+ WRITE_ONCE(transfer->elems, start);
+ WRITE_ONCE(slots->elems, BUDDY_CACHE_BATCH_SIZE);
+
+out_unlock:
+ arena_spin_unlock_irqrestore(&transfer->lock, flags);
+ return ret;
+}
+
+static int buddy_transfer_put_batch(struct buddy __arena *buddy, int order,
+ struct buddy_cache_order __arena *slots)
+{
+ struct buddy_transfer_order __arena *transfer = &buddy->transfer[order];
+ unsigned long flags;
+ u32 elems, local_elems, start;
+ unsigned int i;
+ int ret = 0;
+
+ if (arena_spin_lock_irqsave(&transfer->lock, flags))
+ return -EAGAIN;
+
+ elems = READ_ONCE(transfer->elems);
+ if (elems > BUDDY_TRANSFER_CAPACITY - BUDDY_CACHE_BATCH_SIZE) {
+ ret = -ENOMEM;
+ goto out_unlock;
+ }
+
+ local_elems = READ_ONCE(slots->elems);
+ if (local_elems < BUDDY_CACHE_BATCH_SIZE) {
+ ret = -EINVAL;
+ goto out_unlock;
+ }
+
+ start = local_elems - BUDDY_CACHE_BATCH_SIZE;
+ for (i = zero; i < BUDDY_CACHE_BATCH_SIZE && can_loop; i++)
+ WRITE_ONCE(transfer->objects[elems + i],
+ READ_ONCE(slots->objects[start + i]));
+
+ WRITE_ONCE(slots->elems, start);
+ WRITE_ONCE(transfer->elems, elems + BUDDY_CACHE_BATCH_SIZE);
+
+out_unlock:
+ arena_spin_unlock_irqrestore(&transfer->lock, flags);
+ return ret;
+}
+
/*
- * Refill an empty cache with one trip to the buddy allocator. Kept out of line
- * so that cache hits do not pay for the refill path.
+ * Refill an empty cache from the shared transfer array, or with one trip to the
+ * buddy allocator. Kept out of line so that cache hits do not pay for the refill
+ * path.
*/
static __noinline void __arena *
buddy_cache_refill(struct buddy __arena *buddy, struct buddy_cache __arena *cache,
@@ -136,6 +205,11 @@ buddy_cache_refill(struct buddy __arena *buddy, struct buddy_cache __arena *cach
{
struct buddy_cache_order __arena *slots = &cache->orders[order];
+ /* Peek without the lock, buddy_transfer_take_batch() rechecks under it. */
+ if (READ_ONCE(buddy->transfer[order].elems) >= BUDDY_CACHE_BATCH_SIZE &&
+ !buddy_transfer_take_batch(buddy, order, slots))
+ return buddy_cache_pop(cache, order);
+
if (buddy_alloc_bulk(buddy, size, BUDDY_CACHE_BATCH_ORDER,
(void __arena * __arena *)slots->objects))
return NULL;
@@ -191,6 +265,42 @@ static int buddy_cache_get_order(void __arena *addr)
return idx_get_order(chunk, idx);
}
+/*
+ * Make room in a full cache by moving its top batch to the shared transfer
+ * array, or to the buddy allocator when the array is full or busy, then cache
+ * the freed object. Kept out of line so that frees that fit in the cache do not
+ * pay for it.
+ */
+static __noinline int
+buddy_cache_spill(struct buddy __arena *buddy, struct buddy_cache __arena *cache,
+ int order, void __arena *addr)
+{
+ struct buddy_cache_order __arena *slots = &cache->orders[order];
+ u32 elems;
+ int ret = -ENOMEM;
+
+ /*
+ * Peek without the lock, buddy_transfer_put_batch() rechecks under it.
+ * NMIs never take the transfer lock.
+ */
+ if (!bpf_in_nmi() &&
+ READ_ONCE(buddy->transfer[order].elems) <=
+ BUDDY_TRANSFER_CAPACITY - BUDDY_CACHE_BATCH_SIZE)
+ ret = buddy_transfer_put_batch(buddy, order, slots);
+
+ if (ret) {
+ elems = READ_ONCE(slots->elems) - BUDDY_CACHE_BATCH_SIZE;
+ ret = buddy_free_bulk(buddy, (void __arena * __arena *)&slots->objects[elems],
+ BUDDY_CACHE_BATCH_SIZE);
+ if (ret)
+ return ret;
+
+ WRITE_ONCE(slots->elems, elems);
+ }
+
+ return buddy_cache_push(cache, order, addr);
+}
+
int buddy_cache_free(struct buddy __arena *buddy, void __arena *addr)
{
struct buddy_cache __arena *cache;
@@ -210,6 +320,8 @@ int buddy_cache_free(struct buddy __arena *buddy, void __arena *addr)
return -EAGAIN;
ret = buddy_cache_push(cache, order, addr);
+ if (ret)
+ ret = buddy_cache_spill(buddy, cache, order, addr);
buddy_cache_exit(cache);
return ret;
@@ -741,6 +853,9 @@ static int buddy_cache_init(struct buddy __arena *buddy)
if (CONFIG_NR_CPUS > BUDDY_CACHE_MAX_CPUS)
return -E2BIG;
+ for (order = zero; order < BUDDY_CACHE_NUM_ORDERS && can_loop; order++)
+ WRITE_ONCE(buddy->transfer[order].elems, 0);
+
for (cpu = zero; cpu < CONFIG_NR_CPUS && can_loop; cpu++) {
ret = buddy_alloc_bulk(buddy, sizeof(*cache), 0,
(void __arena * __arena *)&buddy->percpu[cpu]);
@@ -758,10 +873,12 @@ static int buddy_cache_init(struct buddy __arena *buddy)
static void buddy_cache_destroy(struct buddy __arena *buddy)
{
- unsigned int cpu;
+ unsigned int cpu, order;
for (cpu = zero; cpu < min(CONFIG_NR_CPUS, BUDDY_CACHE_MAX_CPUS) && can_loop; cpu++)
WRITE_ONCE(buddy->percpu[cpu], NULL);
+ for (order = zero; order < BUDDY_CACHE_NUM_ORDERS && can_loop; order++)
+ WRITE_ONCE(buddy->transfer[order].elems, 0);
}
__weak int buddy_init(struct buddy __arena *buddy)
--
2.52.0
^ permalink raw reply related [flat|nested] 11+ messages in thread
* [PATCH bpf-next v2 7/7] selftests/bpf: libarena: Cover buddy cache fast paths
2026-10-21 23:26 [PATCH bpf-next v2 0/7] bpf libarena allocator improvements Emil Tsalapatis
` (5 preceding siblings ...)
2026-10-21 23:26 ` [PATCH bpf-next v2 6/7] selftests/bpf: libarena: Add a transfer cache Emil Tsalapatis
@ 2026-10-21 23:26 ` Emil Tsalapatis
6 siblings, 0 replies; 11+ messages in thread
From: Emil Tsalapatis @ 2026-10-21 23:26 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 | 262 ++++++++++++++++++
.../selftests/bpf/libarena/src/buddy.bpf.c | 5 +
2 files changed, 267 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..a25326a3b9a0 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,248 @@ __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;
+
+ ret = buddy_free(&buddy, alloc_path_test.bulk[0]);
+ if (ret)
+ goto out;
+
+ ret = buddy_free(&buddy, alloc_path_test.bulk[1]);
+ if (ret)
+ goto out;
+
+ for (i = 2;
+ 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 80f2a49f95e5..bd52fd9c3146 100644
--- a/tools/testing/selftests/bpf/libarena/src/buddy.bpf.c
+++ b/tools/testing/selftests/bpf/libarena/src/buddy.bpf.c
@@ -1172,6 +1172,11 @@ int buddy_alloc_bulk(struct buddy __arena *buddy, size_t size, size_t norder,
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);
if (!address)
--
2.52.0
^ permalink raw reply related [flat|nested] 11+ messages in thread
* Re: [PATCH bpf-next v2 3/7] selftests/bpf: libarena: Add buddy per-CPU caches
2026-10-21 23:26 ` [PATCH bpf-next v2 3/7] selftests/bpf: libarena: Add buddy per-CPU caches Emil Tsalapatis
@ 2026-10-22 12:53 ` Harry Yoo
2026-10-22 23:40 ` Emil Tsalapatis
0 siblings, 1 reply; 11+ messages in thread
From: Harry Yoo @ 2026-10-22 12:53 UTC (permalink / raw)
To: Emil Tsalapatis; +Cc: bpf, ast, andrii, eddyz87, memxor, daniel
On Wed, Oct 21, 2026 at 11:26:37PM +0000, Emil Tsalapatis wrote:
> 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, bounding each
> CPU's cache to about 1.5 KiB.
TIL the min alloc size is not PAGE_SIZE in BPF buddy allocator :)
> 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 | 215 +++++++++++++++++-
> 4 files changed, 257 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 f150e1c37e3d..66349915745b 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 = 1024,
Is there no way to get nr_cpu_ids from the kernel in BPF?
NR_CPUS is upper bound supported by the kernel but during the boot
process, the kernel updates nr_cpu_ids based on the processor of the
system.
Kernels built with CONFIG_MAXSMP won't be able to use this because
it uses very high NR_CPUS? (8192 on x86)
> + /*
> + * Objects retained locally for each allocation order.
> + */
> + BUDDY_CACHE_CAPACITY = 16,
> +};
> +
> diff --git a/tools/testing/selftests/bpf/libarena/src/buddy.bpf.c b/tools/testing/selftests/bpf/libarena/src/buddy.bpf.c
> index 6dfaa23089a9..c41e163b014f 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 @@
> /*
> * Buddy allocator arena-based implementation.
> *
> +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;
Maybe use cmpxchg_local() instead of cmpxchg() because this is percpu?
Not sure if there's cmpxchg_local() equivalent in BPF...
On x86, cmpxchg instruction should be cheaper without LOCK prefix.
> +
> + return cache;
> +}
> +
> +static __always_inline void buddy_cache_exit(struct buddy_cache __arena *cache)
> +{
> + smp_store_release(&cache->active, 0);
> +}
We don't need this barrier because this is percpu?
--
Cheers,
Harry / Hyeonggon
^ permalink raw reply [flat|nested] 11+ messages in thread
* Re: [PATCH bpf-next v2 3/7] selftests/bpf: libarena: Add buddy per-CPU caches
2026-10-22 12:53 ` Harry Yoo
@ 2026-10-22 23:40 ` Emil Tsalapatis
2026-10-22 23:52 ` Kumar Kartikeya Dwivedi
0 siblings, 1 reply; 11+ messages in thread
From: Emil Tsalapatis @ 2026-10-22 23:40 UTC (permalink / raw)
To: Harry Yoo, Emil Tsalapatis; +Cc: bpf, ast, andrii, eddyz87, memxor, daniel
On Thu Oct 22, 2026 at 12:53 PM UTC, Harry Yoo wrote:
> On Wed, Oct 21, 2026 at 11:26:37PM +0000, Emil Tsalapatis wrote:
>> 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, bounding each
>> CPU's cache to about 1.5 KiB.
>
> TIL the min alloc size is not PAGE_SIZE in BPF buddy allocator :)
>
>> 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 | 215 +++++++++++++++++-
>> 4 files changed, 257 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 f150e1c37e3d..66349915745b 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 = 1024,
>
> Is there no way to get nr_cpu_ids from the kernel in BPF?
> NR_CPUS is upper bound supported by the kernel but during the boot
> process, the kernel updates nr_cpu_ids based on the processor of the
> system.
>
> Kernels built with CONFIG_MAXSMP won't be able to use this because
> it uses very high NR_CPUS? (8192 on x86)
Agreed, but we should make the change in tandem with the arena spinlock code
since it is also hardcoding the same constant. In fact, we should probably
restructure the headers to make sure they are always in lockstep. I didn't
do it in this series because I was planning it as a followup, we can also
add the refactor as a preparatory commit here.
>
>> + /*
>> + * Objects retained locally for each allocation order.
>> + */
>> + BUDDY_CACHE_CAPACITY = 16,
>> +};
>> +
>> diff --git a/tools/testing/selftests/bpf/libarena/src/buddy.bpf.c b/tools/testing/selftests/bpf/libarena/src/buddy.bpf.c
>> index 6dfaa23089a9..c41e163b014f 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 @@
>> /*
>> * Buddy allocator arena-based implementation.
>> *
>> +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;
>
> Maybe use cmpxchg_local() instead of cmpxchg() because this is percpu?
> Not sure if there's cmpxchg_local() equivalent in BPF...
We don't really have cmpxchg_local() available in BPF. I hacked up a prototype
and the benefits were actually not that significant, about 2ns for hot alloc
free loops.
>
> On x86, cmpxchg instruction should be cheaper without LOCK prefix.
>
>> +
>> + return cache;
>> +}
>> +
>> +static __always_inline void buddy_cache_exit(struct buddy_cache __arena *cache)
>> +{
>> + smp_store_release(&cache->active, 0);
>> +}
>
> We don't need this barrier because this is percpu?
... this, however did matter significantly for hot loop frees. I will add it
to v3 and respin.
^ permalink raw reply [flat|nested] 11+ messages in thread
* Re: [PATCH bpf-next v2 3/7] selftests/bpf: libarena: Add buddy per-CPU caches
2026-10-22 23:40 ` Emil Tsalapatis
@ 2026-10-22 23:52 ` Kumar Kartikeya Dwivedi
0 siblings, 0 replies; 11+ messages in thread
From: Kumar Kartikeya Dwivedi @ 2026-10-22 23:52 UTC (permalink / raw)
To: Emil Tsalapatis, Harry Yoo; +Cc: bpf, ast, andrii, eddyz87, daniel
On Fri Oct 23, 2026 at 1:40 AM CEST, Emil Tsalapatis wrote:
> On Thu Oct 22, 2026 at 12:53 PM UTC, Harry Yoo wrote:
>> On Wed, Oct 21, 2026 at 11:26:37PM +0000, Emil Tsalapatis wrote:
>>> 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, bounding each
>>> CPU's cache to about 1.5 KiB.
>>
>> TIL the min alloc size is not PAGE_SIZE in BPF buddy allocator :)
>>
>>> 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>
>>> ---
> [...]
>
> We don't really have cmpxchg_local() available in BPF. I hacked up a prototype
> and the benefits were actually not that significant, about 2ns for hot alloc
> free loops.
>
>>
>> On x86, cmpxchg instruction should be cheaper without LOCK prefix.
>>
>>> +
>>> + return cache;
>>> +}
>>> +
>>> +static __always_inline void buddy_cache_exit(struct buddy_cache __arena *cache)
>>> +{
>>> + smp_store_release(&cache->active, 0);
>>> +}
>>
>> We don't need this barrier because this is percpu?
>
>
> ... this, however did matter significantly for hot loop frees. I will add it
> to v3 and respin.
What architecture are you measuring it on? Because on x86 smp_store_release() is
equivalent to a WRITE_ONCE(), so unless this was suppressing compiler
optimizations and the code changed by doing a simple store, I don't follow how
it would help.
^ permalink raw reply [flat|nested] 11+ messages in thread
end of thread, other threads:[~2026-10-22 23:53 UTC | newest]
Thread overview: 11+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-21 23:26 [PATCH bpf-next v2 0/7] bpf libarena allocator improvements Emil Tsalapatis
2026-10-21 23:26 ` [PATCH bpf-next v2 1/7] selftests/bpf: libarena: Add bulk buddy allocation helpers Emil Tsalapatis
2026-10-21 23:26 ` [PATCH bpf-next v2 2/7] selftests/bpf: Add a parallel buddy allocator benchmark Emil Tsalapatis
2026-10-21 23:26 ` [PATCH bpf-next v2 3/7] selftests/bpf: libarena: Add buddy per-CPU caches Emil Tsalapatis
2026-10-22 12:53 ` Harry Yoo
2026-10-22 23:40 ` Emil Tsalapatis
2026-10-22 23:52 ` Kumar Kartikeya Dwivedi
2026-10-21 23:26 ` [PATCH bpf-next v2 4/7] selftests/bpf: libarena: Handle NMI reentrancy Emil Tsalapatis
2026-10-21 23:26 ` [PATCH bpf-next v2 5/7] selftests/bpf: libarena: Refill local caches in batches Emil Tsalapatis
2026-10-21 23:26 ` [PATCH bpf-next v2 6/7] selftests/bpf: libarena: Add a transfer cache Emil Tsalapatis
2026-10-21 23:26 ` [PATCH bpf-next v2 7/7] selftests/bpf: libarena: Cover buddy cache fast paths Emil Tsalapatis
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.