[PATCH 5/6] selftests/bpf: libarena: Add a benchmark for malloc()/calloc()

Emil Tsalapatis <[email protected]>
Newsgroups org.kernel.vger.bpf
Message-ID <[email protected]>
Add a benchmark for measuring the performance of the malloc()/calloc()
arena allocator calls. This is useful as a basic allocator performance
check that we can easily expand later. As with the regular arena
allocator, focus on sub-page allocations that cannot be satisfied
efficiently with the BPF arena page allocation code.

Signed-off-by: Emil Tsalapatis <[email protected]>
---
 tools/testing/selftests/bpf/Makefile          |  18 +-
 tools/testing/selftests/bpf/bench.c           |   6 +
 .../selftests/bpf/benchs/bench_libarena.c     | 210 ++++++++++++++++++
 .../bpf/benchs/run_bench_libarena.sh          |  31 +++
 tools/testing/selftests/bpf/libarena/Makefile |  30 ++-
 .../bpf/libarena/benchs/bench_malloc.bpf.c    |  51 +++++
 6 files changed, 334 insertions(+), 12 deletions(-)
 create mode 100644 tools/testing/selftests/bpf/benchs/bench_libarena.c
 create mode 100755 tools/testing/selftests/bpf/benchs/run_bench_libarena.sh
 create mode 100644 tools/testing/selftests/bpf/libarena/benchs/bench_malloc.bpf.c

diff --git a/tools/testing/selftests/bpf/Makefile b/tools/testing/selftests/bpf/Makefile
index d3655a706482..22e9eb16c151 100644
--- a/tools/testing/selftests/bpf/Makefile
+++ b/tools/testing/selftests/bpf/Makefile
@@ -826,18 +826,22 @@ LIBARENA_MAKE_ARGS = \
 		BPF_TARGET_ENDIAN="$(BPF_TARGET_ENDIAN)" \
 		Q="$(Q)"
 
-LIBARENA_BPF_DEPS := $(wildcard libarena/Makefile		\
-				 libarena/include/*		\
-				 libarena/include/libarena/*	\
-				 libarena/src/*			\
-				 libarena/selftests/*		\
-				 libarena/*.bpf.o)
+LIBARENA_COMMON_DEPS := $(wildcard libarena/Makefile		\
+				    libarena/include/*		\
+				    libarena/include/libarena/*	\
+				    libarena/src/*)
+LIBARENA_BPF_DEPS := $(LIBARENA_COMMON_DEPS) $(wildcard libarena/selftests/*)
+LIBARENA_BENCH_BPF_DEPS := $(LIBARENA_COMMON_DEPS) $(wildcard libarena/benchs/*)
 
 LIBARENA_SKEL := libarena/libarena.skel.h
+LIBARENA_BENCH_SKEL := libarena/libarena_bench.skel.h
 
 $(LIBARENA_SKEL): $(INCLUDE_DIR)/vmlinux.h $(BPFOBJ) $(LIBARENA_BPF_DEPS)
 	+$(MAKE) -C libarena libarena.skel.h $(LIBARENA_MAKE_ARGS)
 
+$(LIBARENA_BENCH_SKEL): $(INCLUDE_DIR)/vmlinux.h $(BPFOBJ) $(LIBARENA_BENCH_BPF_DEPS) | $(LIBARENA_SKEL)
+	+$(MAKE) -C libarena benchmarks $(LIBARENA_MAKE_ARGS)
+
 ifneq ($(CLANG_HAS_ARENA_ASAN),)
 LIBARENA_ASAN_SKEL := libarena/libarena_asan.skel.h
 CFLAGS += -DHAS_BPF_ARENA_ASAN
@@ -987,6 +991,7 @@ $(OUTPUT)/bench_sockmap.o: $(OUTPUT)/bench_sockmap_prog.skel.h
 $(OUTPUT)/bench_lpm_trie_map.o: $(OUTPUT)/lpm_trie_bench.skel.h $(OUTPUT)/lpm_trie_map.skel.h
 $(OUTPUT)/bench_bpf_nop.o: $(OUTPUT)/bpf_nop_bench.skel.h bench_bpf_timing.h
 $(OUTPUT)/bench_xdp_lb.o: $(OUTPUT)/xdp_lb_bench.skel.h bench_bpf_timing.h
+$(OUTPUT)/bench_libarena.o: $(LIBARENA_BENCH_SKEL)
 $(OUTPUT)/bench_bpf_timing.o: bench_bpf_timing.h
 $(OUTPUT)/bench.o: bench.h testing_helpers.h $(BPFOBJ)
 $(OUTPUT)/bench: LDLIBS += -lm
@@ -1014,6 +1019,7 @@ $(OUTPUT)/bench: $(OUTPUT)/bench.o \
 		 $(OUTPUT)/bench_bpf_timing.o \
 		 $(OUTPUT)/bench_bpf_nop.o \
 		 $(OUTPUT)/bench_xdp_lb.o \
+		 $(OUTPUT)/bench_libarena.o \
 		 $(OUTPUT)/usdt_1.o \
 		 $(OUTPUT)/usdt_2.o \
 		 #
diff --git a/tools/testing/selftests/bpf/bench.c b/tools/testing/selftests/bpf/bench.c
index b86b73456d3c..de672f61d4ab 100644
--- a/tools/testing/selftests/bpf/bench.c
+++ b/tools/testing/selftests/bpf/bench.c
@@ -288,6 +288,7 @@ extern struct argp bench_crypto_argp;
 extern struct argp bench_sockmap_argp;
 extern struct argp bench_lpm_trie_map_argp;
 extern struct argp bench_xdp_lb_argp;
+extern struct argp bench_libarena_argp;
 
 static const struct argp_child bench_parsers[] = {
 	{ &bench_ringbufs_argp, 0, "Ring buffers benchmark", 0 },
@@ -306,6 +307,7 @@ static const struct argp_child bench_parsers[] = {
 	{ &bench_sockmap_argp, 0, "bpf sockmap benchmark", 0 },
 	{ &bench_lpm_trie_map_argp, 0, "LPM trie map benchmark", 0 },
 	{ &bench_xdp_lb_argp, 0, "XDP load-balancer benchmark", 0 },
+	{ &bench_libarena_argp, 0, "libarena allocator benchmark", 0 },
 	{},
 };
 
@@ -585,6 +587,8 @@ extern const struct bench bench_lpm_trie_delete;
 extern const struct bench bench_lpm_trie_free;
 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;
 
 static const struct bench *benchs[] = {
 	&bench_count_global,
@@ -669,6 +673,8 @@ static const struct bench *benchs[] = {
 	&bench_lpm_trie_free,
 	&bench_bpf_nop,
 	&bench_xdp_lb,
+	&bench_libarena_malloc,
+	&bench_libarena_calloc,
 };
 
 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
new file mode 100644
index 000000000000..24e432244bf5
--- /dev/null
+++ b/tools/testing/selftests/bpf/benchs/bench_libarena.c
@@ -0,0 +1,210 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */
+#include <argp.h>
+#include <limits.h>
+#include <string.h>
+
+#include "bench.h"
+
+#include <libarena/common.h>
+#include <libarena/asan.h>
+#include <libarena/buddy.h>
+#include <libarena/userspace.h>
+
+#include "libarena/libarena_bench.skel.h"
+
+static struct {
+	__u64 alloc_size;
+	__u64 nallocs;
+} args = {
+	.alloc_size = 64,
+	.nallocs = 10000,
+};
+
+static struct {
+	struct libarena_bench *skel;
+	int bench_fd;
+	int reset_fd;
+} ctx;
+
+enum {
+	ARG_LIBARENA_ALLOC_SIZE = 12000,
+	ARG_LIBARENA_NALLOCS,
+};
+
+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" },
+	{},
+};
+
+static error_t parse_arg(int key, char *arg, struct argp_state *state)
+{
+	unsigned long value;
+
+	switch (key) {
+	case ARG_LIBARENA_ALLOC_SIZE:
+		value = strtoull(arg, NULL, 10);
+		if (!value || value >= UINT_MAX) {
+			fprintf(stderr, "invalid alloc_size: %ld", value);
+			argp_usage(state);
+		}
+		args.alloc_size = value;
+		break;
+	case ARG_LIBARENA_NALLOCS:
+		args.nallocs = strtoull(arg, NULL, 10);
+		break;
+	default:
+		return ARGP_ERR_UNKNOWN;
+	}
+
+	return 0;
+}
+
+const struct argp bench_libarena_argp = {
+	.options = opts,
+	.parser = parse_arg,
+};
+
+static void validate(void)
+{
+	if (env.consumer_cnt != 0) {
+		fprintf(stderr, "benchmark doesn't support consumers\n");
+		exit(1);
+	}
+
+	if (env.producer_cnt != 1) {
+		fprintf(stderr, "benchmark supports exactly one producer\n");
+		exit(1);
+	}
+}
+
+static void setup_common(void)
+{
+	struct arena_alloc_reserve_args reserve_args = {
+		.nr_pages = ARENA_RESERVE_PAGES_DFL,
+	};
+	int err;
+
+	setup_libbpf();
+
+	ctx.skel = libarena_bench__open_and_load();
+	if (!ctx.skel) {
+		fprintf(stderr, "failed to open and load skeleton\n");
+		exit(1);
+	}
+
+	err = libarena_run_prog_args(
+		bpf_program__fd(ctx.skel->progs.arena_alloc_reserve),
+		&reserve_args, sizeof(reserve_args));
+	if (err) {
+		fprintf(stderr, "failed to reserve arena pages: %d\n", err);
+		exit(1);
+	}
+
+	err = libarena_run_prog(
+		bpf_program__fd(ctx.skel->progs.arena_buddy_reset));
+	if (err) {
+		fprintf(stderr, "failed to initialize arena allocator: %d\n", err);
+		exit(1);
+	}
+
+	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);
+}
+
+static void malloc_setup(void)
+{
+	setup_common();
+	ctx.bench_fd = bpf_program__fd(ctx.skel->progs.bench_malloc);
+}
+
+static void calloc_setup(void)
+{
+	setup_common();
+	ctx.bench_fd = bpf_program__fd(ctx.skel->progs.bench_calloc);
+}
+
+static void *producer(void *input)
+{
+	int err;
+
+	while (true) {
+		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);
+		}
+	}
+
+	return NULL;
+}
+
+static void measure(struct bench_res *res)
+{
+	res->duration_ns = atomic_swap(&ctx.skel->bss->bench_duration_ns, 0);
+	res->hits = atomic_swap(&ctx.skel->bss->bench_hits, 0);
+}
+
+static void report_progress(int iter, struct bench_res *res, long delta_ns)
+{
+	double latency_ns = 0.0;
+
+	if (res->hits)
+		latency_ns = res->duration_ns / (double)res->hits;
+
+	printf("Iter %3d (%7.3lfus): latency %8.3lf ns/op (%ld allocations)\n",
+	       iter, (delta_ns - 1000000000) / 1000.0, latency_ns, res->hits);
+}
+
+static void report_final(struct bench_res res[], int res_cnt)
+{
+	unsigned long duration_ns = 0;
+	long hits = 0;
+	int i;
+
+	for (i = 0; i < res_cnt; i++) {
+		duration_ns += res[i].duration_ns;
+		hits += res[i].hits;
+	}
+
+	if (!hits || !res_cnt) {
+		printf("Summary: no runs measured\n");
+		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);
+}
+
+const struct bench bench_libarena_malloc = {
+	.name = "libarena-malloc",
+	.argp = &bench_libarena_argp,
+	.validate = validate,
+	.setup = malloc_setup,
+	.producer_thread = producer,
+	.measure = measure,
+	.report_progress = report_progress,
+	.report_final = report_final,
+};
+
+const struct bench bench_libarena_calloc = {
+	.name = "libarena-calloc",
+	.argp = &bench_libarena_argp,
+	.validate = validate,
+	.setup = calloc_setup,
+	.producer_thread = producer,
+	.measure = measure,
+	.report_progress = report_progress,
+	.report_final = report_final,
+};
diff --git a/tools/testing/selftests/bpf/benchs/run_bench_libarena.sh b/tools/testing/selftests/bpf/benchs/run_bench_libarena.sh
new file mode 100755
index 000000000000..10afe4d52ebf
--- /dev/null
+++ b/tools/testing/selftests/bpf/benchs/run_bench_libarena.sh
@@ -0,0 +1,31 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+
+source ./benchs/run_common.sh
+
+set -eufo pipefail
+
+RUN_BENCH="./bench -d3 -q"
+
+summarize_libarena()
+{
+	local bench="$1"
+	local summary
+
+	summary=$(printf '%s\n' "$2" | tail -n1)
+	summary=${summary#Summary: }
+	printf "%-20s %s\n" "$bench" "$summary"
+}
+
+header "libarena sequential malloc\n"
+
+for size in 16 64 256 1024 4096; do
+subtitle "allocation size: $size"
+printf "\t-------------------\n"
+	for nallocs in 10 50 100 500 1000 5000 10000; do
+		summarize_libarena "malloc:" \
+			"$($RUN_BENCH --alloc_size "$size" --nallocs "$nallocs" libarena-malloc)"
+		summarize_libarena "calloc:" \
+			"$($RUN_BENCH --alloc_size "$size" --nallocs "$nallocs" libarena-calloc)"
+	done
+done
diff --git a/tools/testing/selftests/bpf/libarena/Makefile b/tools/testing/selftests/bpf/libarena/Makefile
index 5e2ab514805e..6d62eda34920 100644
--- a/tools/testing/selftests/bpf/libarena/Makefile
+++ b/tools/testing/selftests/bpf/libarena/Makefile
@@ -27,10 +27,17 @@ BPFDIR=$(abspath $(LIBARENA)/..)
 INCLUDE_DIR ?= $(BPFDIR)/tools/include
 LIBBPF_INCLUDE ?= $(INCLUDE_DIR)
 
-# Scan src/ and selftests/ to generate the final binaries
-LIBARENA_SOURCES = $(wildcard $(LIBARENA)/src/*.bpf.c) $(wildcard $(LIBARENA)/selftests/*.bpf.c)
-LIBARENA_OBJECTS = $(notdir $(LIBARENA_SOURCES:.bpf.c=.bpf.o))
-LIBARENA_OBJECTS_ASAN = $(notdir $(LIBARENA_SOURCES:.bpf.c=_asan.bpf.o))
+# Build selftests and benchmarks into separate BPF objects and skeletons.
+LIBARENA_CORE_SOURCES = $(wildcard $(LIBARENA)/src/*.bpf.c)
+LIBARENA_TEST_SOURCES = $(wildcard $(LIBARENA)/selftests/*.bpf.c)
+LIBARENA_BENCH_SOURCES = $(wildcard $(LIBARENA)/benchs/*.bpf.c)
+
+LIBARENA_OBJECTS = $(notdir $(LIBARENA_CORE_SOURCES:.bpf.c=.bpf.o) \
+			       $(LIBARENA_TEST_SOURCES:.bpf.c=.bpf.o))
+LIBARENA_OBJECTS_ASAN = $(notdir $(LIBARENA_CORE_SOURCES:.bpf.c=_asan.bpf.o) \
+				    $(LIBARENA_TEST_SOURCES:.bpf.c=_asan.bpf.o))
+LIBARENA_BENCH_OBJECTS = $(notdir $(LIBARENA_CORE_SOURCES:.bpf.c=.bpf.o) \
+				     $(LIBARENA_BENCH_SOURCES:.bpf.c=.bpf.o))
 
 INCLUDES = -I$(LIBARENA)/include -I$(BPFDIR)
 ifneq ($(INCLUDE_DIR),)
@@ -58,12 +65,19 @@ override BPF_CFLAGS += $(INCLUDES)
 CFLAGS = -O2 -no-pie
 CFLAGS += $(INCLUDES)
 
-vpath %.bpf.c $(LIBARENA)/src $(LIBARENA)/selftests
-vpath %.c $(LIBARENA)/src $(LIBARENA)/selftests
+vpath %.bpf.c $(LIBARENA)/src $(LIBARENA)/selftests $(LIBARENA)/benchs
+vpath %.c $(LIBARENA)/src $(LIBARENA)/selftests $(LIBARENA)/benchs
 
 skeletons: libarena.skel.h libarena_asan.skel.h
 .PHONY: skeletons
 
+benchmarks: libarena_bench.skel.h
+.PHONY: benchmarks
+
+libarena_bench.skel.h: libarena_bench.bpf.o
+	$(call msg,GEN-SKEL,libarena,$@)
+	$(Q)$(BPFTOOL) gen skeleton $< name "libarena_bench" > $@
+
 libarena_asan.skel.h: libarena_asan.bpf.o
 	$(call msg,GEN-SKEL,libarena,$@)
 	$(Q)$(BPFTOOL) gen skeleton $< name "libarena_asan" > $@
@@ -80,6 +94,10 @@ libarena.bpf.o: $(LIBARENA_OBJECTS)
 	$(call msg,GEN-OBJ,libarena,$@)
 	$(Q)$(BPFTOOL) gen object $@ $^
 
+libarena_bench.bpf.o: $(LIBARENA_BENCH_OBJECTS)
+	$(call msg,GEN-OBJ,libarena,$@)
+	$(Q)$(BPFTOOL) gen object $@ $^
+
 %_asan.bpf.o: %.bpf.c
 	$(call msg,CLNG-BPF,libarena,$@)
 	$(Q)$(CLANG) $(BPF_CFLAGS) $(ASAN_FLAGS) -DBPF_ARENA_ASAN $(BPF_TARGET_ENDIAN) -c $< -o $@
diff --git a/tools/testing/selftests/bpf/libarena/benchs/bench_malloc.bpf.c b/tools/testing/selftests/bpf/libarena/benchs/bench_malloc.bpf.c
new file mode 100644
index 000000000000..e06397dc9a40
--- /dev/null
+++ b/tools/testing/selftests/bpf/libarena/benchs/bench_malloc.bpf.c
@@ -0,0 +1,51 @@
+// SPDX-License-Identifier: LGPL-2.1 OR BSD-2-Clause
+/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */
+#include <libarena/common.h>
+
+#include <libarena/asan.h>
+#include <libarena/buddy.h>
+
+u32 bench_alloc_size;
+u32 bench_nallocs;
+long bench_hits;
+long bench_duration_ns;
+
+SEC("syscall")
+int bench_malloc(void)
+{
+	void __arena *mem;
+	u64 start_ns;
+	u32 i;
+
+	start_ns = bpf_ktime_get_ns();
+	for (i = zero; i < bench_nallocs && can_loop; i++) {
+		mem = arena_malloc(bench_alloc_size);
+		if (!mem)
+			return -ENOMEM;
+	}
+
+	__sync_add_and_fetch(&bench_duration_ns,
+			     bpf_ktime_get_ns() - start_ns);
+	__sync_add_and_fetch(&bench_hits, i);
+	return 0;
+}
+
+SEC("syscall")
+int bench_calloc(void)
+{
+	void __arena *mem;
+	u64 start_ns;
+	u32 i;
+
+	start_ns = bpf_ktime_get_ns();
+	for (i = zero; i < bench_nallocs && can_loop; i++) {
+		mem = arena_calloc(1, bench_alloc_size);
+		if (!mem)
+			return -ENOMEM;
+	}
+
+	__sync_add_and_fetch(&bench_duration_ns,
+			     bpf_ktime_get_ns() - start_ns);
+	__sync_add_and_fetch(&bench_hits, i);
+	return 0;
+}
-- 
2.54.0
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.