Skip to content

Commit ed411d8

Browse files
mflemingKernel Patches Daemon
authored andcommitted
selftests/bpf: Add LPM trie microbenchmarks
Add benchmarks for the standard set of operations: lookup, update, delete. Also, include a benchmark for trie_free() which is known to have terrible performance for maps with many entries. Benchmarks operate on tries without gaps in the key range, i.e. each test begins with a trie with valid keys in the range [0, nr_entries). This is intended to cause maximum branching when traversing the trie. All measurements are recorded inside the kernel to remove syscall overhead. Most benchmarks run an XDP program to generate stats but free needs to collect latencies using fentry/fexit on map_free_deferred() because it's not possible to use fentry directly on lpm_trie.c since commit c83508d ("bpf: Avoid deadlock caused by nested kprobe and fentry bpf programs") and there's no way to create/destroy a map from within an XDP program. Here is example output from an AMD EPYC 9684X 96-Core machine for each of the benchmarks using a trie with 10K entries and a 32-bit prefix length, e.g. $ ./bench lpm-trie-$op \ --prefix_len=32 \ --producers=1 \ --nr_entries=10000 lookup: throughput 7.423 ± 0.023 M ops/s ( 7.423M ops/prod), latency 134.710 ns/op update: throughput 2.643 ± 0.015 M ops/s ( 2.643M ops/prod), latency 378.310 ns/op delete: throughput 0.712 ± 0.008 M ops/s ( 0.712M ops/prod), latency 1405.152 ns/op free: throughput 0.574 ± 0.003 K ops/s ( 0.574K ops/prod), latency 1.743 ms/op Tested-by: Jesper Dangaard Brouer <[email protected]> Reviewed-by: Jesper Dangaard Brouer <[email protected]> Signed-off-by: Matt Fleming <[email protected]>
1 parent 4ccf98a commit ed411d8

File tree

6 files changed

+540
-0
lines changed

6 files changed

+540
-0
lines changed

tools/testing/selftests/bpf/Makefile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -816,6 +816,7 @@ $(OUTPUT)/bench_bpf_hashmap_lookup.o: $(OUTPUT)/bpf_hashmap_lookup.skel.h
816816
$(OUTPUT)/bench_htab_mem.o: $(OUTPUT)/htab_mem_bench.skel.h
817817
$(OUTPUT)/bench_bpf_crypto.o: $(OUTPUT)/crypto_bench.skel.h
818818
$(OUTPUT)/bench_sockmap.o: $(OUTPUT)/bench_sockmap_prog.skel.h
819+
$(OUTPUT)/bench_lpm_trie_map.o: $(OUTPUT)/lpm_trie_bench.skel.h $(OUTPUT)/lpm_trie_map.skel.h
819820
$(OUTPUT)/bench.o: bench.h testing_helpers.h $(BPFOBJ)
820821
$(OUTPUT)/bench: LDLIBS += -lm
821822
$(OUTPUT)/bench: $(OUTPUT)/bench.o \
@@ -837,6 +838,7 @@ $(OUTPUT)/bench: $(OUTPUT)/bench.o \
837838
$(OUTPUT)/bench_htab_mem.o \
838839
$(OUTPUT)/bench_bpf_crypto.o \
839840
$(OUTPUT)/bench_sockmap.o \
841+
$(OUTPUT)/bench_lpm_trie_map.o \
840842
#
841843
$(call msg,BINARY,,$@)
842844
$(Q)$(CC) $(CFLAGS) $(LDFLAGS) $(filter %.a %.o,$^) $(LDLIBS) -o $@

tools/testing/selftests/bpf/bench.c

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,7 @@ extern struct argp bench_htab_mem_argp;
284284
extern struct argp bench_trigger_batch_argp;
285285
extern struct argp bench_crypto_argp;
286286
extern struct argp bench_sockmap_argp;
287+
extern struct argp bench_lpm_trie_map_argp;
287288

288289
static const struct argp_child bench_parsers[] = {
289290
{ &bench_ringbufs_argp, 0, "Ring buffers benchmark", 0 },
@@ -299,6 +300,7 @@ static const struct argp_child bench_parsers[] = {
299300
{ &bench_trigger_batch_argp, 0, "BPF triggering benchmark", 0 },
300301
{ &bench_crypto_argp, 0, "bpf crypto benchmark", 0 },
301302
{ &bench_sockmap_argp, 0, "bpf sockmap benchmark", 0 },
303+
{ &bench_lpm_trie_map_argp, 0, "LPM trie map benchmark", 0 },
302304
{},
303305
};
304306

@@ -558,6 +560,10 @@ extern const struct bench bench_htab_mem;
558560
extern const struct bench bench_crypto_encrypt;
559561
extern const struct bench bench_crypto_decrypt;
560562
extern const struct bench bench_sockmap;
563+
extern const struct bench bench_lpm_trie_lookup;
564+
extern const struct bench bench_lpm_trie_update;
565+
extern const struct bench bench_lpm_trie_delete;
566+
extern const struct bench bench_lpm_trie_free;
561567

562568
static const struct bench *benchs[] = {
563569
&bench_count_global,
@@ -625,6 +631,10 @@ static const struct bench *benchs[] = {
625631
&bench_crypto_encrypt,
626632
&bench_crypto_decrypt,
627633
&bench_sockmap,
634+
&bench_lpm_trie_lookup,
635+
&bench_lpm_trie_update,
636+
&bench_lpm_trie_delete,
637+
&bench_lpm_trie_free,
628638
};
629639

630640
static void find_benchmark(void)

tools/testing/selftests/bpf/bench.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ struct bench_res {
4646
unsigned long gp_ns;
4747
unsigned long gp_ct;
4848
unsigned int stime;
49+
unsigned long duration_ns;
4950
};
5051

5152
struct bench {
Lines changed: 337 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,337 @@
1+
// SPDX-License-Identifier: GPL-2.0
2+
/* Copyright (c) 2025 Cloudflare */
3+
4+
/*
5+
* All of these benchmarks operate on tries with keys in the range
6+
* [0, args.nr_entries), i.e. there are no gaps or partially filled
7+
* branches of the trie for any key < args.nr_entries.
8+
*
9+
* This gives an idea of worst-case behaviour.
10+
*/
11+
12+
#include <argp.h>
13+
#include <linux/time64.h>
14+
#include <linux/if_ether.h>
15+
#include "lpm_trie_bench.skel.h"
16+
#include "lpm_trie_map.skel.h"
17+
#include "bench.h"
18+
#include "testing_helpers.h"
19+
20+
static struct ctx {
21+
struct lpm_trie_bench *bench;
22+
} ctx;
23+
24+
static struct {
25+
__u32 nr_entries;
26+
__u32 prefixlen;
27+
} args = {
28+
.nr_entries = 10000,
29+
.prefixlen = 32,
30+
};
31+
32+
enum {
33+
ARG_NR_ENTRIES = 9000,
34+
ARG_PREFIX_LEN,
35+
};
36+
37+
static const struct argp_option opts[] = {
38+
{ "nr_entries", ARG_NR_ENTRIES, "NR_ENTRIES", 0,
39+
"Number of unique entries in the LPM trie" },
40+
{ "prefix_len", ARG_PREFIX_LEN, "PREFIX_LEN", 0,
41+
"Number of prefix bits to use in the LPM trie" },
42+
{},
43+
};
44+
45+
static error_t lpm_parse_arg(int key, char *arg, struct argp_state *state)
46+
{
47+
long ret;
48+
49+
switch (key) {
50+
case ARG_NR_ENTRIES:
51+
ret = strtol(arg, NULL, 10);
52+
if (ret < 1 || ret > UINT_MAX) {
53+
fprintf(stderr, "Invalid nr_entries count.");
54+
argp_usage(state);
55+
}
56+
args.nr_entries = ret;
57+
break;
58+
case ARG_PREFIX_LEN:
59+
ret = strtol(arg, NULL, 10);
60+
if (ret < 1 || ret > UINT_MAX) {
61+
fprintf(stderr, "Invalid prefix_len value.");
62+
argp_usage(state);
63+
}
64+
args.prefixlen = ret;
65+
break;
66+
default:
67+
return ARGP_ERR_UNKNOWN;
68+
}
69+
return 0;
70+
}
71+
72+
const struct argp bench_lpm_trie_map_argp = {
73+
.options = opts,
74+
.parser = lpm_parse_arg,
75+
};
76+
77+
static void __lpm_validate(void)
78+
{
79+
if (env.consumer_cnt != 0) {
80+
fprintf(stderr, "benchmark doesn't support consumer!\n");
81+
exit(1);
82+
}
83+
84+
if ((1UL << args.prefixlen) < args.nr_entries) {
85+
fprintf(stderr, "prefix_len value too small for nr_entries!\n");
86+
exit(1);
87+
};
88+
}
89+
90+
enum { OP_LOOKUP = 1, OP_UPDATE, OP_DELETE, OP_FREE };
91+
92+
static void lpm_delete_validate(void)
93+
{
94+
__lpm_validate();
95+
96+
if (env.producer_cnt != 1) {
97+
fprintf(stderr,
98+
"lpm-trie-delete requires a single producer!\n");
99+
exit(1);
100+
}
101+
}
102+
103+
static void lpm_free_validate(void)
104+
{
105+
__lpm_validate();
106+
107+
if (env.producer_cnt != 1) {
108+
fprintf(stderr, "lpm-trie-free requires a single producer!\n");
109+
exit(1);
110+
}
111+
}
112+
113+
static void fill_map(int map_fd)
114+
{
115+
int i, err;
116+
117+
for (i = 0; i < args.nr_entries; i++) {
118+
struct trie_key {
119+
__u32 prefixlen;
120+
__u32 data;
121+
} key = { args.prefixlen, i };
122+
__u32 val = 1;
123+
124+
err = bpf_map_update_elem(map_fd, &key, &val, BPF_NOEXIST);
125+
if (err) {
126+
fprintf(stderr, "failed to add key %d to map: %d\n",
127+
key.data, -err);
128+
exit(1);
129+
}
130+
}
131+
}
132+
133+
static void __lpm_setup(void)
134+
{
135+
ctx.bench = lpm_trie_bench__open_and_load();
136+
if (!ctx.bench) {
137+
fprintf(stderr, "failed to open skeleton\n");
138+
exit(1);
139+
}
140+
141+
ctx.bench->bss->nr_entries = args.nr_entries;
142+
ctx.bench->bss->prefixlen = args.prefixlen;
143+
144+
if (lpm_trie_bench__attach(ctx.bench)) {
145+
fprintf(stderr, "failed to attach skeleton\n");
146+
exit(1);
147+
}
148+
}
149+
150+
static void lpm_setup(void)
151+
{
152+
int fd;
153+
154+
__lpm_setup();
155+
156+
fd = bpf_map__fd(ctx.bench->maps.trie_map);
157+
fill_map(fd);
158+
}
159+
160+
static void lpm_lookup_setup(void)
161+
{
162+
lpm_setup();
163+
164+
ctx.bench->bss->op = OP_LOOKUP;
165+
}
166+
167+
static void lpm_update_setup(void)
168+
{
169+
lpm_setup();
170+
171+
ctx.bench->bss->op = OP_UPDATE;
172+
}
173+
174+
static void lpm_delete_setup(void)
175+
{
176+
lpm_setup();
177+
178+
ctx.bench->bss->op = OP_DELETE;
179+
}
180+
181+
static void lpm_free_setup(void)
182+
{
183+
__lpm_setup();
184+
ctx.bench->bss->op = OP_FREE;
185+
}
186+
187+
static void lpm_measure(struct bench_res *res)
188+
{
189+
res->hits = atomic_swap(&ctx.bench->bss->hits, 0);
190+
res->duration_ns = atomic_swap(&ctx.bench->bss->duration_ns, 0);
191+
}
192+
193+
/* For LOOKUP, UPDATE, and DELETE */
194+
static void *lpm_producer(void *unused __always_unused)
195+
{
196+
int err;
197+
char in[ETH_HLEN]; /* unused */
198+
199+
LIBBPF_OPTS(bpf_test_run_opts, opts, .data_in = in,
200+
.data_size_in = sizeof(in), .repeat = 1, );
201+
202+
while (true) {
203+
int fd = bpf_program__fd(ctx.bench->progs.run_bench);
204+
err = bpf_prog_test_run_opts(fd, &opts);
205+
if (err) {
206+
fprintf(stderr, "failed to run BPF prog: %d\n", err);
207+
exit(1);
208+
}
209+
210+
if (opts.retval < 0) {
211+
fprintf(stderr, "BPF prog returned error: %d\n",
212+
opts.retval);
213+
exit(1);
214+
}
215+
216+
if (ctx.bench->bss->op == OP_DELETE && opts.retval == 1) {
217+
/* trie_map needs to be refilled */
218+
fill_map(bpf_map__fd(ctx.bench->maps.trie_map));
219+
}
220+
}
221+
222+
return NULL;
223+
}
224+
225+
static void *lpm_free_producer(void *unused __always_unused)
226+
{
227+
while (true) {
228+
struct lpm_trie_map *skel;
229+
230+
skel = lpm_trie_map__open_and_load();
231+
if (!skel) {
232+
fprintf(stderr, "failed to open skeleton\n");
233+
exit(1);
234+
}
235+
236+
fill_map(bpf_map__fd(skel->maps.trie_free_map));
237+
lpm_trie_map__destroy(skel);
238+
}
239+
240+
return NULL;
241+
}
242+
243+
static void free_ops_report_progress(int iter, struct bench_res *res,
244+
long delta_ns)
245+
{
246+
double hits_per_sec, hits_per_prod;
247+
double rate_divisor = 1000.0;
248+
char rate = 'K';
249+
250+
hits_per_sec = res->hits / (res->duration_ns / (double)NSEC_PER_SEC) /
251+
rate_divisor;
252+
hits_per_prod = hits_per_sec / env.producer_cnt;
253+
254+
printf("Iter %3d (%7.3lfus): ", iter,
255+
(delta_ns - NSEC_PER_SEC) / 1000.0);
256+
printf("hits %8.3lf%c/s (%7.3lf%c/prod)\n", hits_per_sec, rate,
257+
hits_per_prod, rate);
258+
}
259+
260+
static void free_ops_report_final(struct bench_res res[], int res_cnt)
261+
{
262+
double hits_mean = 0.0, hits_stddev = 0.0;
263+
double lat_divisor = 1000000.0;
264+
double rate_divisor = 1000.0;
265+
const char *unit = "ms";
266+
double latency = 0.0;
267+
char rate = 'K';
268+
int i;
269+
270+
for (i = 0; i < res_cnt; i++) {
271+
double val = res[i].hits / rate_divisor /
272+
(res[i].duration_ns / (double)NSEC_PER_SEC);
273+
hits_mean += val / (0.0 + res_cnt);
274+
latency += res[i].duration_ns / res[i].hits / (0.0 + res_cnt);
275+
}
276+
277+
if (res_cnt > 1) {
278+
for (i = 0; i < res_cnt; i++) {
279+
double val =
280+
res[i].hits / rate_divisor /
281+
(res[i].duration_ns / (double)NSEC_PER_SEC);
282+
hits_stddev += (hits_mean - val) * (hits_mean - val) /
283+
(res_cnt - 1.0);
284+
}
285+
286+
hits_stddev = sqrt(hits_stddev);
287+
}
288+
printf("Summary: throughput %8.3lf \u00B1 %5.3lf %c ops/s (%7.3lf%c ops/prod), ",
289+
hits_mean, hits_stddev, rate, hits_mean / env.producer_cnt,
290+
rate);
291+
printf("latency %8.3lf %s/op\n",
292+
latency / lat_divisor / env.producer_cnt, unit);
293+
}
294+
295+
const struct bench bench_lpm_trie_lookup = {
296+
.name = "lpm-trie-lookup",
297+
.argp = &bench_lpm_trie_map_argp,
298+
.validate = __lpm_validate,
299+
.setup = lpm_lookup_setup,
300+
.producer_thread = lpm_producer,
301+
.measure = lpm_measure,
302+
.report_progress = ops_report_progress,
303+
.report_final = ops_report_final,
304+
};
305+
306+
const struct bench bench_lpm_trie_update = {
307+
.name = "lpm-trie-update",
308+
.argp = &bench_lpm_trie_map_argp,
309+
.validate = __lpm_validate,
310+
.setup = lpm_update_setup,
311+
.producer_thread = lpm_producer,
312+
.measure = lpm_measure,
313+
.report_progress = ops_report_progress,
314+
.report_final = ops_report_final,
315+
};
316+
317+
const struct bench bench_lpm_trie_delete = {
318+
.name = "lpm-trie-delete",
319+
.argp = &bench_lpm_trie_map_argp,
320+
.validate = lpm_delete_validate,
321+
.setup = lpm_delete_setup,
322+
.producer_thread = lpm_producer,
323+
.measure = lpm_measure,
324+
.report_progress = ops_report_progress,
325+
.report_final = ops_report_final,
326+
};
327+
328+
const struct bench bench_lpm_trie_free = {
329+
.name = "lpm-trie-free",
330+
.argp = &bench_lpm_trie_map_argp,
331+
.validate = lpm_free_validate,
332+
.setup = lpm_free_setup,
333+
.producer_thread = lpm_free_producer,
334+
.measure = lpm_measure,
335+
.report_progress = free_ops_report_progress,
336+
.report_final = free_ops_report_final,
337+
};

0 commit comments

Comments
 (0)