[RFC PATCH] ftrace: speed up module enable by binary searching module symbols
From: Yaxiong Tian <hidden>
Date: 2026-09-16 08:27:29
Also in:
lkml
Subsystem:
function hooks (ftrace), the rest, tracing · Maintainers:
Steven Rostedt, Masami Hiramatsu, Linus Torvalds
Loading a module with many functions takes seconds, almost all of it in ftrace_module_enable(). amdgpu modules stall for about ten seconds, and disabling CONFIG_DYNAMIC_FTRACE makes the stall disappear. Every record is validated by test_for_valid_rec(), which resolves its ip through kallsyms_lookup(); for a module address that ends up in find_kallsyms_symbol(), which walks the whole module symbol table from index 1. The core kernel instead binary searches its sorted kallsyms table in get_symbol_pos(). So only modules pay, and the cost is quadratic: both the record count and the symbol count grow with the module. An amdgpu.ko from this tree has 17282 records and 55332 symbol table entries. Fix this by collecting the addresses of a module's symbols once, sorting them, and binary searching that array instead of walking the table once per record; a loaded module's symbol table cannot change, so the array stays valid for the whole pass. What comes back is what the linear scan returned: the same entries are skipped - SHN_UNDEF, unnamed and mapping symbols - and the closest address not below the base of the record's memory region is taken. Whenever the array cannot answer, because it could not be built or because no such symbol exists, the record is handed to test_for_valid_rec(), so the rest of kallsyms_lookup() still has its say and no verdict can change. On the machine that reported the stall, modprobe of that amdgpu.ko takes 12.242 s before and 2.870 s after, and the time spent inside ftrace_lock drops from 9.368 s to 1.585 ms - 15.195 ms if the price of building the array is included, that build being 13.6 ms, 0.15% of the 9.37 s saved. The array costs O(symbols * log symbols), it does not grow with the record count. Across the 74 modules loaded at boot, total lock time drops from 491 ms to 2.33 ms. Signed-off-by: Yaxiong Tian <redacted> --- kernel/trace/ftrace.c | 131 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 130 insertions(+), 1 deletion(-)
diff --git a/kernel/trace/ftrace.c b/kernel/trace/ftrace.c
index 673a54fdf392..92ba9a4dfa69 100644
--- a/kernel/trace/ftrace.c
+++ b/kernel/trace/ftrace.c@@ -26,6 +26,7 @@ #include <linux/uaccess.h> #include <linux/bsearch.h> #include <linux/module.h> +#include <linux/module_symbol.h> #include <linux/ftrace.h> #include <linux/sysctl.h> #include <linux/slab.h>
@@ -8010,11 +8011,137 @@ void ftrace_release_mod(struct module *mod) } } +#ifdef FTRACE_MCOUNT_MAX_OFFSET +/* + * ftrace_module_enable() validates every record with test_for_valid_rec(), + * which walks the whole symbol table of a module for each address in it, so a + * large module loads in quadratic time. Search a sorted copy of the symbol + * addresses instead, the way get_symbol_pos() searches the kallsyms table. + * The array is an accelerator only; records it cannot resolve are tested the + * old way, so the verdict does not change. + */ +struct ftrace_mod_syms { + unsigned int nr; + unsigned long addr[]; /* sorted, @nr entries */ +}; + +static int ftrace_mod_syms_cmp(const void *a, const void *b) +{ + unsigned long va = *(const unsigned long *)a; + unsigned long vb = *(const unsigned long *)b; + + return (va > vb) - (va < vb); +} + +static struct ftrace_mod_syms *ftrace_mod_syms_alloc(struct module *mod) +{ + struct mod_kallsyms *kallsyms; + struct ftrace_mod_syms *syms; + unsigned int i, nr = 0; + + /* mod->kallsyms is not replaced until do_init_module() returns. */ + rcu_read_lock(); + kallsyms = rcu_dereference(mod->kallsyms); + rcu_read_unlock(); + + if (!kallsyms || kallsyms->num_symtab < 2) + return NULL; + + syms = kvmalloc(struct_size(syms, addr, kallsyms->num_symtab), + GFP_KERNEL); + if (!syms) + return NULL; + + /* The symbols find_kallsyms_symbol() would consider. */ + for (i = 1; i < kallsyms->num_symtab; i++) { + const Elf_Sym *sym = &kallsyms->symtab[i]; + const char *name = kallsyms->strtab + sym->st_name; + + if (sym->st_shndx == SHN_UNDEF || !*name || + is_mapping_symbol(name)) + continue; + + syms->addr[nr++] = kallsyms_symbol_value(sym); + } + syms->nr = nr; + + if (nr > 1) + sort(syms->addr, nr, sizeof(syms->addr[0]), ftrace_mod_syms_cmp, + NULL); + + return syms; +} + +/* The verdict test_for_valid_rec() would give, without walking the table. */ +static bool ftrace_mod_rec_is_valid(struct module *mod, + const struct ftrace_mod_syms *syms, + struct dyn_ftrace *rec) +{ + unsigned long base, best; + unsigned int lo, hi; + + if (!syms) + return test_for_valid_rec(rec) != 0; + + /* A record is in executable memory; anything else kallsyms answers. */ + if (within_module_mem_type(rec->ip, mod, MOD_TEXT)) + base = (unsigned long)mod->mem[MOD_TEXT].base; + else if (within_module_mem_type(rec->ip, mod, MOD_INIT_TEXT)) + base = (unsigned long)mod->mem[MOD_INIT_TEXT].base; + else + return test_for_valid_rec(rec) != 0; + + /* The closest address at or below @rec->ip, if there is one. */ + best = 0; + for (lo = 0, hi = syms->nr; lo < hi; ) { + unsigned int mid = lo + (hi - lo) / 2; + + if (syms->addr[mid] <= rec->ip) + lo = mid + 1; + else + hi = mid; + } + if (lo) + best = syms->addr[lo - 1]; + + /* + * No symbol, or only ones below @base: let kallsyms_lookup() answer, + * as it also looks in the BPF and ftrace trampoline tables. + */ + if (best < base) + return test_for_valid_rec(rec) != 0; + + return rec->ip - best <= FTRACE_MCOUNT_MAX_OFFSET; +} +#else +struct ftrace_mod_syms; + +static inline struct ftrace_mod_syms *ftrace_mod_syms_alloc(struct module *mod) +{ + return NULL; +} + +static inline bool ftrace_mod_rec_is_valid(struct module *mod, + const struct ftrace_mod_syms *syms, + struct dyn_ftrace *rec) +{ + return test_for_valid_rec(rec) != 0; +} +#endif /* FTRACE_MCOUNT_MAX_OFFSET */ + void ftrace_module_enable(struct module *mod) { + struct ftrace_mod_syms *syms; struct dyn_ftrace *rec; struct ftrace_page *pg; + /* + * Allocates, so build it before taking ftrace_lock. It is skipped when + * ftrace never came up, where the loop below would not run anyway; that + * also makes reading ftrace_disabled without the lock safe. + */ + syms = ftrace_disabled ? NULL : ftrace_mod_syms_alloc(mod); + mutex_lock(&ftrace_lock); if (ftrace_disabled)
@@ -8050,7 +8177,7 @@ void ftrace_module_enable(struct module *mod) cond_resched(); /* Weak functions should still be ignored */ - if (!test_for_valid_rec(rec)) { + if (!ftrace_mod_rec_is_valid(mod, syms, rec)) { /* Clear all other flags. Should not be enabled anyway */ rec->flags = FTRACE_FL_DISABLED; continue;
@@ -8087,6 +8214,8 @@ void ftrace_module_enable(struct module *mod) out_unlock: mutex_unlock(&ftrace_lock); + kvfree(syms); + process_cached_mods(mod->name); }
--
2.43.0