mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
From: Jim Cromie <jim.cromie@gmail.com>
To: Andrew Morton <akpm@linux-foundation.org>
Cc: Lorenzo Stoakes <ljs@kernel.org>, Kees Cook <kees@kernel.org>,
	 David Laight <david.laight.linux@gmail.com>,
	 Masahiro Yamada <masahiroy@kernel.org>,
	linux-kernel@vger.kernel.org,  linux-kbuild@vger.kernel.org,
	bpf@vger.kernel.org,  Jim Cromie <jim.cromie@gmail.com>
Subject: [PATCH v4 3/4] kallsyms: Add dynamic lookup index for batch resolution
Date: Tue, 22 Sep 2026 14:08:20 -0600	[thread overview]
Message-ID: <20260922-ksyms-tune-v4-3-92acea84b911@gmail.com> (raw)
In-Reply-To: <20260922-ksyms-tune-v4-0-92acea84b911@gmail.com>

kallsyms_lookup_names() resolves symbol names to addresses using a
binary search over kallsyms_seqs_of_names[].  In baseline, each step
invokes get_symbol_offset(), which scans sequentially from the nearest
256-symbol marker in kallsyms_names[] (averaging ~128 ULEB128 header
decodes per probe, ~2,176 decodes per lookup).

During bulk symbol resolution workloads (e.g. BPF multi-kprobe / fprobe
tracing attaching across tens of thousands of functions), this linear
scan compounds into substantial kernel attach latency (~3.3 us per
lookup).  Baking a permanent direct index into .rodata accelerates
lookups but consumes hundreds of KiB of unswappable kernel image memory,
which is unjustified when bulk lookups are infrequent.

Introduce a dynamic u32 lookup index populated on demand in transient
RAM and discarded when the batch workload completes:

0. Expose kallsyms_lookup_batch_start() and kallsyms_lookup_batch_end()
   bracketed by a mutex and refcount.  On the first caller, allocate a
   flat u32 table spanning all kallsyms_num_syms entries (~736 KiB in
   transient RAM for ~184k symbols) via kvmalloc_array() and populate
   the symbol offsets via a single sequential scan in ~150 us.

1. In get_symbol_offset(), when the dynamic table is active, return
   offsets[pos] directly as an O(1) array access with 0 hops, bracketed
   by rcu_read_lock() and rcu_read_unlock().

2. If the dynamic table is unallocated (refcount 0), fall back to the
   legacy kallsyms_markers[] scan, preserving 100% safety for oops,
   panic, NMI, and low-memory environments without any .rodata bloat.

3. Wrap kallsyms_lookup_names() in rcu_read_lock() / rcu_read_unlock()
   to ensure lockless lifetime safety across multi-probe searches.

4. Update test_kallsyms_perf and kallsyms_selftest to benchmark both
   unindexed marker scans and the dynamic batch index side by side,
   reporting batch lookup performance alongside query amortization
   break-even points.

Signed-off-by: Jim Cromie <jim.cromie@gmail.com>
---
Changes in v4:
- Annotate dyn_kallsyms_offsets declaration with __rcu to satisfy sparse
  type checking and prevent address-space warnings across
  rcu_assign_pointer() and rcu_dereference() (addresses Sashiko AI review).
- Use rcu_replace_pointer() with lockdep_is_held(&dyn_kallsyms_mutex)
  during batch teardown to atomically read and clear the pointer while
  satisfying sparse address-space constraints (addresses Sashiko AI review).

Changes in v3:
- Reorder patch after on-the-fly token matching (patch 2) to cleanly
  isolate and measure the impact of the O(1) dynamic table on top of
  fast string matching (addresses David Laight review).
- Consume get_symbol_data() helper introduced in patch 2 to preserve
  clean git bisectability (addresses Sashiko AI review).
- Fix use-after-free race on dyn_kallsyms_offsets: bracket table
  dereference and array read with rcu_read_lock() and replace
  rcu_dereference_raw() with rcu_dereference() inside
  get_symbol_offset() to protect external readers (lookup_symbol_name,
  kallsyms_lookup_buildid, reset_iter) against concurrent batch
  teardown (addresses Sashiko AI review).
- In kallsyms_selftest, add a second lookup pass bracketed by
  kallsyms_lookup_batch_start() and kallsyms_lookup_batch_end() to
  validate batch resolution in the in-tree selftest.
- Move 24-bit loop unrolling in get_symbol_seq() to standalone patch 4
  (addresses David Laight review).
- Move David Laight to series-wide Cc on cover letter, dropping trailer
  from this patch.

Changes in v2:
- Rework from static build-time 3-byte table to dynamic u32 index
  allocated in transient RAM during batch sessions, dropping +573 KiB
  from .rodata to 0 bytes (addresses Kees Cook review).
- Use native u32 indexing to eliminate 24-bit big-endian shifts and
  unaligned loads (addresses David Laight review).
- Revert changes to scripts/kallsyms.c and kernel/kallsyms_internal.h,
  retaining legacy kallsyms_markers[] as fallback.
---
 include/linux/kallsyms.h   |  13 ++++++
 kernel/kallsyms.c          | 111 ++++++++++++++++++++++++++++++++++++++++++---
 kernel/kallsyms_selftest.c |  16 +++++++
 lib/test_kallsyms_perf.c   |  51 ++++++++++++++++++---
 4 files changed, 179 insertions(+), 12 deletions(-)

diff --git a/include/linux/kallsyms.h b/include/linux/kallsyms.h
index d5dd54c53ace..6e39795ac509 100644
--- a/include/linux/kallsyms.h
+++ b/include/linux/kallsyms.h
@@ -74,6 +74,10 @@ int kallsyms_on_each_symbol(int (*fn)(void *, const char *, unsigned long),
 int kallsyms_on_each_match_symbol(int (*fn)(void *, unsigned long),
 				  const char *name, void *data);
 
+/* Transient dynamic lookup index bracketing */
+int kallsyms_lookup_batch_start(void);
+void kallsyms_lookup_batch_end(void);
+
 /* Lookup the address for a symbol. Returns 0 if not found. */
 unsigned long kallsyms_lookup_name(const char *name);
 
@@ -164,6 +168,15 @@ static inline int kallsyms_on_each_match_symbol(int (*fn)(void *, unsigned long)
 {
 	return -EOPNOTSUPP;
 }
+
+static inline int kallsyms_lookup_batch_start(void)
+{
+	return 0;
+}
+
+static inline void kallsyms_lookup_batch_end(void)
+{
+}
 #endif /*CONFIG_KALLSYMS*/
 
 static inline void print_ip_sym(const char *loglvl, unsigned long ip)
diff --git a/kernel/kallsyms.c b/kernel/kallsyms.c
index d18d78e626db..bb34b4c0f690 100644
--- a/kernel/kallsyms.c
+++ b/kernel/kallsyms.c
@@ -22,6 +22,9 @@
 #include <linux/sched.h>	/* for cond_resched */
 #include <linux/ctype.h>
 #include <linux/slab.h>
+#include <linux/vmalloc.h>
+#include <linux/mutex.h>
+#include <linux/rcupdate.h>
 #include <linux/filter.h>
 #include <linux/ftrace.h>
 #include <linux/kprobes.h>
@@ -90,7 +93,7 @@ static unsigned int kallsyms_expand_symbol(unsigned int off,
 	if (maxlen)
 		*result = '\0';
 
-	/* Return to offset to the next symbol. */
+	/* Return offset to the next symbol. */
 	return off;
 }
 
@@ -139,12 +142,11 @@ static int kallsyms_strcmp_symbol(unsigned int off, const char *name)
 	return (unsigned char)*name;
 }
 
-
 /*
- * Find the offset on the compressed stream given and index in the
- * kallsyms array.
+ * Find the offset on the compressed stream given an index in the
+ * kallsyms array using legacy markers.
  */
-static unsigned int get_symbol_offset(unsigned long pos)
+static unsigned int get_symbol_offset_marker(unsigned long pos)
 {
 	const u8 *name;
 	int i, len;
@@ -177,6 +179,99 @@ static unsigned int get_symbol_offset(unsigned long pos)
 	return name - kallsyms_names;
 }
 
+/*
+ * Dynamic symbol offset table.
+ * Allocated on demand during high-volume lookup batches and reclaimed after.
+ */
+static u32 __rcu *dyn_kallsyms_offsets __read_mostly;
+static DEFINE_MUTEX(dyn_kallsyms_mutex);
+static unsigned int dyn_kallsyms_refcnt;
+
+static u32 *kallsyms_build_offsets(void)
+{
+	const u8 *name = kallsyms_names;
+	u32 *offsets;
+	unsigned int i;
+
+	offsets = kvmalloc_array(kallsyms_num_syms, sizeof(u32), GFP_KERNEL);
+	if (!offsets)
+		return NULL;
+
+	for (i = 0; i < kallsyms_num_syms; i++) {
+		unsigned int len;
+		const u8 *data;
+
+		offsets[i] = name - kallsyms_names;
+		data = get_symbol_data(offsets[i], &len);
+		name = data + len;
+	}
+
+	return offsets;
+}
+
+int kallsyms_lookup_batch_start(void)
+{
+	int ret = 0;
+
+	mutex_lock(&dyn_kallsyms_mutex);
+	if (!dyn_kallsyms_refcnt) {
+		u32 *offsets = kallsyms_build_offsets();
+
+		if (!offsets) {
+			ret = -ENOMEM;
+			goto out;
+		}
+		rcu_assign_pointer(dyn_kallsyms_offsets, offsets);
+	}
+	dyn_kallsyms_refcnt++;
+out:
+	mutex_unlock(&dyn_kallsyms_mutex);
+	return ret;
+}
+EXPORT_SYMBOL_GPL(kallsyms_lookup_batch_start);
+
+void kallsyms_lookup_batch_end(void)
+{
+	u32 *offsets = NULL;
+
+	mutex_lock(&dyn_kallsyms_mutex);
+	if (WARN_ON(!dyn_kallsyms_refcnt))
+		goto out;
+
+	if (--dyn_kallsyms_refcnt == 0)
+		offsets = rcu_replace_pointer(dyn_kallsyms_offsets, NULL,
+					      lockdep_is_held(&dyn_kallsyms_mutex));
+out:
+	mutex_unlock(&dyn_kallsyms_mutex);
+
+	if (offsets) {
+		synchronize_rcu();
+		kvfree(offsets);
+	}
+}
+EXPORT_SYMBOL_GPL(kallsyms_lookup_batch_end);
+
+/*
+ * Find the offset on the compressed table given an index in the
+ * kallsyms array.
+ */
+static inline unsigned int get_symbol_offset(unsigned long pos)
+{
+	unsigned int off;
+	u32 *offsets;
+
+	rcu_read_lock();
+	offsets = rcu_dereference(dyn_kallsyms_offsets);
+	if (offsets) {
+		off = offsets[pos];
+		rcu_read_unlock();
+		return off;
+	}
+	rcu_read_unlock();
+
+	return get_symbol_offset_marker(pos);
+}
+
 unsigned long kallsyms_sym_address(int idx)
 {
 	/* non-relocatable 32-bit kernels just embed the value directly */
@@ -206,6 +301,7 @@ static int kallsyms_lookup_names(const char *name,
 	low = 0;
 	high = kallsyms_num_syms - 1;
 
+	rcu_read_lock();
 	while (low <= high) {
 		mid = low + (high - low) / 2;
 		seq = get_symbol_seq(mid);
@@ -219,8 +315,10 @@ static int kallsyms_lookup_names(const char *name,
 			break;
 	}
 
-	if (low > high)
+	if (low > high) {
+		rcu_read_unlock();
 		return -ESRCH;
+	}
 
 	low = mid;
 	while (low) {
@@ -243,6 +341,7 @@ static int kallsyms_lookup_names(const char *name,
 		}
 		*end = high;
 	}
+	rcu_read_unlock();
 
 	return 0;
 }
diff --git a/kernel/kallsyms_selftest.c b/kernel/kallsyms_selftest.c
index 8f6c4e9b3a1c..aedffc058c0e 100644
--- a/kernel/kallsyms_selftest.c
+++ b/kernel/kallsyms_selftest.c
@@ -178,6 +178,7 @@ static int lookup_name(void *data, const char *name, unsigned long addr)
 static void test_perf_kallsyms_lookup_name(void)
 {
 	struct test_stat stat;
+	int ret;
 
 	memset(&stat, 0, sizeof(stat));
 	stat.min = INT_MAX;
@@ -185,6 +186,21 @@ static void test_perf_kallsyms_lookup_name(void)
 	pr_info("kallsyms_lookup_name() looked up %d symbols\n", stat.real_cnt);
 	pr_info("The time spent on each symbol is (ns): min=%d, max=%d, avg=%lld\n",
 		stat.min, stat.max, div_u64(stat.sum, stat.real_cnt));
+
+	ret = kallsyms_lookup_batch_start();
+	if (ret) {
+		pr_err("kallsyms_lookup_batch_start() failed: %d\n", ret);
+		return;
+	}
+
+	memset(&stat, 0, sizeof(stat));
+	stat.min = INT_MAX;
+	kallsyms_on_each_symbol(lookup_name, &stat);
+	kallsyms_lookup_batch_end();
+
+	pr_info("kallsyms_lookup_name() (batch) looked up %d symbols\n", stat.real_cnt);
+	pr_info("The time spent on each symbol is (ns): min=%d, max=%d, avg=%lld\n",
+		stat.min, stat.max, div_u64(stat.sum, stat.real_cnt));
 }
 
 static int find_symbol(void *data, const char *name, unsigned long addr)
diff --git a/lib/test_kallsyms_perf.c b/lib/test_kallsyms_perf.c
index 03ff5f1d51c5..f6c3d5d82e5d 100644
--- a/lib/test_kallsyms_perf.c
+++ b/lib/test_kallsyms_perf.c
@@ -91,7 +91,8 @@ static int count_cb(void *data, const char *name, unsigned long addr)
 	return 0;
 }
 
-static void run_name_lookup_bench(unsigned int iters)
+static void run_name_lookup_bench(const char *mode, unsigned int iters,
+				  u64 *avg_hit_ns, u64 *avg_miss_ns)
 {
 	u64 t0, t1, dt_hit = 0, dt_miss = 0;
 	unsigned long addr = 0;
@@ -153,10 +154,15 @@ static void run_name_lookup_bench(unsigned int iters)
 		dt_miss += t1 - t0;
 	}
 
-	pr_info("Name Search Hit:  %llu ns/lookup (%llu ms total, %u iters)\n",
-		div_u64(dt_hit, iters), div_u64(dt_hit, 1000000), iters);
-	pr_info("Name Search Miss: %llu ns/lookup (%llu ms total, %u iters)\n",
-		div_u64(dt_miss, iters), div_u64(dt_miss, 1000000), iters);
+	if (avg_hit_ns)
+		*avg_hit_ns = div_u64(dt_hit, iters);
+	if (avg_miss_ns)
+		*avg_miss_ns = div_u64(dt_miss, iters);
+
+	pr_info("[%s] Name Search Hit:  %llu ns/lookup (%llu ms total, %u iters)\n",
+		mode, div_u64(dt_hit, iters), div_u64(dt_hit, 1000000), iters);
+	pr_info("[%s] Name Search Miss: %llu ns/lookup (%llu ms total, %u iters)\n",
+		mode, div_u64(dt_miss, iters), div_u64(dt_miss, 1000000), iters);
 }
 
 static void run_address_lookup_bench(unsigned int iters)
@@ -243,6 +249,9 @@ static void run_table_walk_bench(void)
 static int run_kallsyms_benchmark(void)
 {
 	unsigned int iters;
+	u64 unindexed_hit = 0, unindexed_miss = 0;
+	u64 dyn_hit = 0, dyn_miss = 0;
+	u64 t0, t1, dt_start = 0, dt_end = 0;
 	int ret = 0;
 
 	if (!mutex_trylock(&bench_lock)) {
@@ -261,7 +270,37 @@ static int run_kallsyms_benchmark(void)
 	pr_info("Starting kallsyms performance benchmark (iters=%u)\n", iters);
 	pr_info("==================================================\n");
 
-	run_name_lookup_bench(iters);
+	/* 1. Benchmark baseline unindexed marker scan */
+	run_name_lookup_bench("Unindexed (markers)", iters, &unindexed_hit, &unindexed_miss);
+
+	/* 2. Benchmark dynamic lookup index */
+	t0 = ktime_get_ns();
+	ret = kallsyms_lookup_batch_start();
+	t1 = ktime_get_ns();
+	dt_start = t1 - t0;
+
+	if (ret) {
+		pr_err("Failed to start kallsyms lookup batch: %d\n", ret);
+	} else {
+		run_name_lookup_bench("Dynamic Index", iters, &dyn_hit, &dyn_miss);
+
+		t0 = ktime_get_ns();
+		kallsyms_lookup_batch_end();
+		t1 = ktime_get_ns();
+		dt_end = t1 - t0;
+
+		pr_info("[Dynamic Index] Batch setup: %llu us, teardown: %llu us\n",
+			div_u64(dt_start, 1000), div_u64(dt_end, 1000));
+
+		if (unindexed_hit > dyn_hit) {
+			u64 saved = unindexed_hit - dyn_hit;
+
+			pr_info("[Dynamic Index] Amortization break-even: %llu queries (setup), %llu queries (total)\n",
+				DIV_ROUND_UP_ULL(dt_start, saved),
+				DIV_ROUND_UP_ULL(dt_start + dt_end, saved));
+		}
+	}
+
 	run_address_lookup_bench(iters);
 	run_table_walk_bench();
 

-- 
2.55.0


  parent reply	other threads:[~2026-09-22 20:08 UTC|newest]

Thread overview: 7+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-09-22 20:08 [PATCH v4 0/4] kallsyms: Accelerate symbol name lookups by ~19x Jim Cromie
2026-09-22 20:08 ` [PATCH v4 1/4] kallsyms: Add test_kallsyms_perf module to benchmark lookup latency Jim Cromie
2026-09-22 20:08 ` [PATCH v4 2/4] kallsyms: Match compressed tokens on the fly during binary search Jim Cromie
2026-09-22 20:08 ` Jim Cromie [this message]
2026-09-22 20:08 ` [PATCH v4 4/4] kallsyms: Unroll 24-bit sequence reconstruction in get_symbol_seq() Jim Cromie
2026-09-23  7:12 ` [PATCH v4 0/4] kallsyms: Accelerate symbol name lookups by ~19x Kees Cook
2026-09-23 10:00   ` David Laight

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260922-ksyms-tune-v4-3-92acea84b911@gmail.com \
    --to=jim.cromie@gmail.com \
    --cc=akpm@linux-foundation.org \
    --cc=bpf@vger.kernel.org \
    --cc=david.laight.linux@gmail.com \
    --cc=kees@kernel.org \
    --cc=linux-kbuild@vger.kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=ljs@kernel.org \
    --cc=masahiroy@kernel.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox

all inboxes | Powered by JetHome®