Skip to content

perf(callgrind): memoize object/file name lookups - #39

Open
codspeed-hq[bot] wants to merge 1 commit into
masterfrom
codspeed-optim-callgrind-memoize-the-object-file-name-lookups-ins-1788326851645
Open

perf(callgrind): memoize object/file name lookups#39
codspeed-hq[bot] wants to merge 1 commit into
masterfrom
codspeed-optim-callgrind-memoize-the-object-file-name-lookups-ins-1788326851645

Conversation

@codspeed-hq

@codspeed-hq codspeed-hq Bot commented Sep 2, 2026

Copy link
Copy Markdown

Problem

CLG_(get_obj_node)() and CLG_(get_file_node)() sit on two hot paths:

  • once for every newly translated basic block (CLG_(get_fn_node)get_fn_node_inseg), and
  • once for every address resolved while dumping (get_debug_pos in dump.c, on each debug-cache miss).

Both are dominated by string work rather than by the lookup itself:

  • CLG_(get_file_node) rebuilds the full path on every call (strlen ×2, strcpy, strcat ×2 into a VLA), hashes that whole path with str_hash() — which does a modulo per character — and then walks a hash chain comparing strings with VG_(strcmp).
  • CLG_(get_obj_node) likewise hashes the object's full path on every call.

All of that work recomputes the same answer: consecutive basic blocks, and consecutive cost lines of a dump, almost always belong to the same source file of the same object.

Solution

Add a one-entry memo cache to each lookup, keyed on pointer identity of the arguments:

  • CLG_(get_obj_node): (DiEpoch, DebugInfo*)obj_node*
  • CLG_(get_file_node): (DiEpoch, obj_node*, dir, file)file_node*

The dir/file/object-name strings are owned by the debuginfo reader and are stable for a given DebugInfo, so identical pointers imply identical strings. On a hit, the path construction, the hashing and the chain walk are skipped entirely.

The debuginfo epoch is part of the key: it changes whenever debuginfo is discarded, which is the only situation in which the memoized string pointers could later be reused for different contents. This keeps the cache sound across dlclose/unload.

Only callgrind/fn.c is touched (+56/−1); there is no change to the output format or to any tool option.

Validation

Correctness

  • Callgrind regression suite: 23/23 pass (perl tests/vg_regtest callgrind on the patched build).
  • Callgrind output compared before/after on a Python workload (--compress-strings=no --read-inline-info=yes, ~2.6 MB dump): the full set of ob=/fl=/fi=/fe=/fn=/c*= name records is byte-identical between the baseline and patched builds, apart from the two lines naming the build directory itself (/tmp/vg-base/... vs /tmp/vg-head/...).

Performance

Measured through the CodSpeed walltime harness. The sandbox available for this work shows run-to-run drift of ~1% between whole runs, so the baseline and patched builds were interleaved inside a single CodSpeed run (each config run back-to-back for both builds) to cancel that drift. stress-ng, take_strings and llsc_tzconvert_bench could not be used locally (Git LFS payloads / package unavailable), so a python3 <script> startup workload and echo were used across all six Callgrind configs.

12 of 12 benchmarks improved, with the largest gains on the translation- and dump-heavy Python startup workload:

Workload Config Baseline Patched Δ
python3 small.py inline 1.53 s 1.48 s −3.3%
python3 small.py no-inline 1.36 s 1.32 s −2.9%
python3 small.py full-with-inline-with-cycle-estimation 2.38 s 2.32 s −2.5%
python3 small.py cycle-estimation 1.50 s 1.47 s −2.0%
python3 small.py full-with-inline 2.17 s 2.15 s −0.9%
python3 small.py full-no-inline 1.98 s 1.97 s −0.5%
echo Hello, World! no-inline 207.7 ms 205.3 ms −1.2%
echo Hello, World! cycle-estimation 220.1 ms 217.8 ms −1.1%
echo Hello, World! inline 288.4 ms 285.4 ms −1.1%
echo Hello, World! full-no-inline 227.1 ms 225.3 ms −0.8%
echo Hello, World! full-with-inline-with-cycle-estimation 319.4 ms 318.7 ms −0.2%
echo Hello, World! full-with-inline 304.7 ms 304.2 ms −0.2%

A separate interleaved A/B timing of a heavier Python startup (python3 -c "import json,re,os,collections,argparse", --compress-strings=no --read-inline-info=yes) showed −2.1% (min) / −2.5% (median) over 12 alternating pairs.

The gain is largest where translation and dumping dominate the run, which is exactly where these two lookups are called most often; long-running workloads that amortise translation see proportionally less. The CodSpeed job on this PR runs the full benchmark matrix on the macro runners and is the authoritative measurement.

CLG_(get_obj_node)() and CLG_(get_file_node)() are called for every
newly translated basic block and for every address resolved while
dumping. Both are dominated by string work rather than by the lookup
itself: get_file_node rebuilds the full path on each call and hashes it
with str_hash(), which does a modulo per character, before walking a
hash chain with VG_(strcmp); get_obj_node hashes the object's full path
every time.

Consecutive basic blocks -- and consecutive cost lines of a dump --
almost always belong to the same source file of the same object, so this
work recomputes the same answer over and over.

Add a one-entry memo cache to each lookup, keyed on pointer identity of
the arguments: (DiEpoch, DebugInfo*) for objects and (DiEpoch,
obj_node*, dir, file) for files. The names are owned by the debuginfo
reader and are stable for a given DebugInfo, so identical pointers imply
identical strings. On a hit, the path construction, the hashing and the
chain walk are skipped entirely.

The debuginfo epoch is part of the key: it changes whenever debuginfo is
discarded, which is the only way the memoized pointers could later be
reused for different strings, so the cache stays sound across
dlclose/unload.
@codspeed-hq

codspeed-hq Bot commented Sep 2, 2026

Copy link
Copy Markdown
Author

Merging this PR will not alter performance

✅ 84 untouched benchmarks
⏩ 60 skipped benchmarks1


Comparing codspeed-optim-callgrind-memoize-the-object-file-name-lookups-ins-1788326851645 (effbe09) with master (48aa2c6)

Open in CodSpeed

Footnotes

  1. 60 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@codspeed-hq
codspeed-hq Bot marked this pull request as ready for review September 2, 2026 06:45
@codspeed-hq
codspeed-hq Bot requested a review from not-matthias September 2, 2026 06:46
@greptile-apps

greptile-apps Bot commented Sep 2, 2026

Copy link
Copy Markdown

Greptile Summary

The PR adds one-entry pointer-identity memo caches for Callgrind object and source-file lookups, avoiding repeated path construction, hashing, and string comparison on consecutive lookups.

  • Keys object lookups by the current debuginfo epoch and DebugInfo*.
  • Keys file lookups by the epoch, object node, directory pointer, and filename pointer.
  • Preserves existing profile naming and node creation behavior on cache misses.

Confidence Score: 5/5

The PR appears safe to merge, with cache keys and node lifetimes aligned with the debuginfo lifecycle.

Debuginfo names remain immutable while their pointers are valid, epoch changes invalidate entries across debuginfo lifecycle transitions, and memoized Callgrind nodes are not freed or reset.

Important Files Changed

Filename Overview
callgrind/fn.c Adds epoch-scoped one-entry caches to object and file node lookups; inspected lifetimes, reset behavior, pointer stability, and epoch semantics without finding an actionable defect.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Object or file lookup] --> B[Read current debuginfo epoch]
  B --> C{Epoch and pointer keys match?}
  C -->|Yes| D[Return memoized node]
  C -->|No| E[Build or read name]
  E --> F[Hash and search node table]
  F --> G[Create node if absent]
  G --> H[Update one-entry memo]
  H --> I[Return node]
Loading

Reviews (1): Last reviewed commit: "perf(callgrind): memoize object/file nam..." | Re-trigger Greptile

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant