From 2ed8e5bd17d0ebd50820b6e81098274e76d1447b Mon Sep 17 00:00:00 2001 From: CodSpeed Bot Date: Sat, 1 Aug 2026 09:22:52 +0000 Subject: [PATCH] perf(callgrind): cut redundant debug-info address lookups on the dump path Writing the profile at exit queries file/line information for every instruction address of every dumped basic block (~141k addresses for `python3 testdata/test.py`). Almost all of that work is redundant: consecutive addresses belong to the same source line and the same source file. - Add a one-entry range cache to search_all_loctabs(). It answers a query that lands in the last hit loctab entry (or the next one) with a couple of compares instead of walking every DebugInfo and binary searching its loctab. It is only reused within the epoch it was found in and is dropped by advance_current_DiEpoch(), so a stale DebugInfo can never be read. - Return early from VG_(get_inline_fnname) when --read-inline-info=no. No inltab is built in that case, so the answer is always "no inlined function", but the loctab search was still performed for every dumped address. This mirrors the guard already present in VG_(new_IIPC). - Memoize the last file_node in get_debug_pos(). The existing debug cache is keyed by instruction address so it never hits while walking the distinct addresses of a basic block; every miss called CLG_(get_file_node), which rebuilds and rehashes the absolute source path. The memo compares the interned (dir, file) pointers and is reset by init_debug_cache() at the start of each dump. Callgrind output is byte-for-byte identical to the previous build. --- callgrind/dump.c | 39 +++++++++++++++++++- coregrind/m_debuginfo/debuginfo.c | 59 +++++++++++++++++++++++++++++++ 2 files changed, 97 insertions(+), 1 deletion(-) diff --git a/callgrind/dump.c b/callgrind/dump.c index b1e7eab53..3ada6e7b6 100644 --- a/callgrind/dump.c +++ b/callgrind/dump.c @@ -361,6 +361,25 @@ static int debug_cache_line[DEBUG_CACHE_SIZE]; static Bool debug_cache_info[DEBUG_CACHE_SIZE]; static const HChar* debug_cache_inlfn[DEBUG_CACHE_SIZE]; +/* One-entry memo for the last file_node looked up by get_debug_pos(). + * + * The cache above is keyed by instruction address, so it never hits while a + * dump walks the (all distinct) addresses of a basic block. Every one of + * those misses ends in CLG_(get_file_node), which rebuilds the absolute + * source path on the stack and hashes it character by character - even + * though consecutive addresses nearly always belong to the same source file. + * + * The (dir, file) strings come from the debug info and are interned per + * DebugInfo, so the same source file always yields the same pointers and + * comparing pointers is enough. The memo is only used within one dump: no + * guest code runs while dumping, so no debug info can be loaded or discarded + * in between, and init_debug_cache() resets it at the start of every dump. + */ +static obj_node* debug_last_obj; +static const HChar* debug_last_dir; +static const HChar* debug_last_file; +static file_node* debug_last_node; + static __inline__ void init_debug_cache(void) { @@ -372,6 +391,10 @@ void init_debug_cache(void) debug_cache_info[i] = 0; debug_cache_inlfn[i] = 0; } + debug_last_obj = 0; + debug_last_dir = 0; + debug_last_file = 0; + debug_last_node = 0; } static /* __inline__ */ @@ -397,7 +420,21 @@ Bool get_debug_pos(BBCC* bbcc, Addr addr, AddrPos* p) file = "???"; p->line = 0; } - p->file = CLG_(get_file_node)(bbcc->bb->obj, dir, file); + /* Same source file as the previous lookup? Then reuse its node + * instead of rebuilding and rehashing the absolute path. */ + if ((bbcc->bb->obj == debug_last_obj) && + (dir == debug_last_dir) && + (file == debug_last_file)) { + p->file = debug_last_node; + } + else { + p->file = CLG_(get_file_node)(bbcc->bb->obj, dir, file); + + debug_last_obj = bbcc->bb->obj; + debug_last_dir = dir; + debug_last_file = file; + debug_last_node = p->file; + } debug_cache_info[cachepos] = found_file_line; debug_cache_addr[cachepos] = addr; diff --git a/coregrind/m_debuginfo/debuginfo.c b/coregrind/m_debuginfo/debuginfo.c index 97f98b62b..dd55a76a3 100644 --- a/coregrind/m_debuginfo/debuginfo.c +++ b/coregrind/m_debuginfo/debuginfo.c @@ -137,8 +137,13 @@ inline DiEpoch VG_(current_DiEpoch) ( void ) { DiEpoch dep; dep.n = current_epoch; return dep; } +/* Forward */ +static void invalidate_loctab_cache ( void ); + static void advance_current_DiEpoch ( const HChar* msg ) { current_epoch++; + /* Anything cached about the previous epoch's DebugInfos is now stale. */ + invalidate_loctab_cache(); if (DEBUG_EPOCHS) VG_(printf)("Advancing current epoch to %u due to %s\n", current_epoch, msg); @@ -2138,6 +2143,35 @@ static void search_all_symtabs ( DiEpoch ep, Addr ptr, } +/* One-entry cache for search_all_loctabs(). + * + * Callers query addresses with strong locality: dumping a profile asks for + * every instruction of a basic block in turn, and one source line usually + * covers several instructions, so consecutive queries land in the same + * loctab entry or in the one right after it, of the same DebugInfo. + * Remembering the last hit turns those queries into a couple of compares + * instead of a walk over all DebugInfos plus a binary search of the loctab. + * + * The entry is only reused for the epoch it was found in, and it is dropped + * whenever the epoch advances (see advance_current_DiEpoch), i.e. whenever + * debug info is loaded or discarded, so a stale DebugInfo is never read. */ +static DiEpoch loctab_cache_ep; /* zeroed => invalid epoch */ +static DebugInfo* loctab_cache_di = NULL; +static Word loctab_cache_locno = 0; + +static void invalidate_loctab_cache ( void ) +{ + loctab_cache_di = NULL; +} + +/* Does loctab entry `lno` of `di` cover `ptr`? */ +static inline +Bool loctab_entry_covers ( const DebugInfo* di, Word lno, Addr ptr ) +{ + Addr lo = di->loctab[lno].addr; + return ptr >= lo && ptr - lo < (Addr)di->loctab[lno].size; +} + /* Search all loctabs that we know about to locate ptr at epoch ep. If *found, set pdi to the relevant DebugInfo, and *locno to the loctab entry *number within that. If not found, *pdi is set to NULL. */ @@ -2146,6 +2180,25 @@ static void search_all_loctabs ( DiEpoch ep, Addr ptr, { Word lno; DebugInfo* di; + + /* Same entry as last time, or the one following it? */ + if (LIKELY(loctab_cache_di != NULL && eq_DiEpoch(ep, loctab_cache_ep))) { + di = loctab_cache_di; + lno = loctab_cache_locno; + if (LIKELY(loctab_entry_covers(di, lno, ptr))) { + *locno = lno; + *pdi = di; + return; + } + lno++; + if (lno < di->loctab_used && loctab_entry_covers(di, lno, ptr)) { + loctab_cache_locno = lno; + *locno = lno; + *pdi = di; + return; + } + } + for (di = debugInfo_list; di != NULL; di = di->next) { if (!is_DI_valid_for_epoch(di, ep)) continue; @@ -2155,6 +2208,9 @@ static void search_all_loctabs ( DiEpoch ep, Addr ptr, && ptr < di->text_avma + di->text_size) { lno = ML_(search_one_loctab) ( di, ptr ); if (lno == -1) goto not_found; + loctab_cache_ep = ep; + loctab_cache_di = di; + loctab_cache_locno = lno; *locno = lno; *pdi = di; return; @@ -2384,6 +2440,9 @@ Bool VG_(get_inline_fnname) ( DiEpoch ep, Addr a, const HChar** inl_fnname ) DebugInfo* si; Word locno; + if (!VG_(clo_read_inline_info)) + return False; // No inltab was built, so no way to find inlined calls. + /* Find the DebugInfo for this address */ search_all_loctabs(ep, a, &si, &locno); if (si == NULL || si->inltab == NULL)