perf(callgrind): write cost lines straight to the dump file - #36
Conversation
fprint_cost() runs once per cost line of callgrind.out, i.e. tens of thousands of times for a medium sized profile. It went through CLG_(mappingcost_as_string)(), which builds the line in a freshly allocated XArray grown one character at a time, strdup()s it, prints it and frees both -- roughly a dozen heap operations per line for a string thrown away immediately. Format the event costs directly into the VgFile's output buffer instead, using the same value and zero-run-compression logic. The emitted bytes are identical. CLG_(mappingcost_as_string)() is kept for the non-hot callers (summary/totals lines, log output).
Merging this PR will not alter performance
Comparing Footnotes
|
Greptile SummaryReworks Callgrind’s hot cost-line serialization path to write event costs directly to the dump file, avoiding temporary allocation while preserving the existing output format.
Confidence Score: 5/5The PR appears safe to merge with no identified correctness or security issues. The new direct-write implementation preserves the previous serializer’s byte-producing logic for all handled cost shapes while removing per-line temporary allocation.
|
| Filename | Overview |
|---|---|
| callgrind/dump.c | The direct serializer matches the previous formatter’s handling of null or empty costs, event offsets, values, and zero runs; no actionable defect was found. |
Reviews (1): Last reviewed commit: "perf(callgrind): write cost lines straig..." | Re-trigger Greptile
Summary
fprint_cost()incallgrind/dump.cis the innermost step of writing a profile: it runs once per cost line ofcallgrind.out— over 100k times forpython3 testdata/test.pyin the benchmark suite.Each call went through
CLG_(mappingcost_as_string)(), which:XArray,VG_(xaprintf)(repeated realloc + memcpy as it doubles),VG_(strdup)()s the result into a second allocation,XArray,VG_(fprintf)(fp, "%s\n", …),That is roughly a dozen heap operations plus an extra copy of every line, for a string that is discarded immediately.
The change
fprint_cost()now formats the event costs directly into theVgFile's output buffer, using exactly the same value / zero-run-compression logic as before. No heap allocation on the dump's inner path.CLG_(mappingcost_as_string)()is left untouched and is still used by the handful of non-hot callers (summary:/totals:lines, log output).Correctness
The emitted bytes are identical by construction. Verified locally on this branch:
callgrind.outagainst a build of the unpatched parent commit, on a deterministic workload, across 12 option combinations — including--cache-sim=yes,--branch-sim=yes,--simulate-wb,--simulate-hwpref,--dump-instr=yes --dump-line=yes,--collect-jumps=yes,--separate-callers=3,--cycle-estimation=yes, and the CIfull-with-inlineconfiguration. This covers event sets from 1 to 16 events and exercises the skipped-zero compression path. The only bytes that differed were the two builds' own install prefixes inob=/cob=lines.make checkclean, andvg_regtest callgrind cachegrind→ 40 tests, 0 failures.Measurement
Measured through the CodSpeed CLI in walltime mode, comparing a build of the parent commit against this build, on the 9 benchmarks reachable in the sandbox (
echo Hello, World!,python3 testdata/test.py,stress-ng --cpu 1 --cpu-ops 10×no-inline/inline/full-with-inline), 8 rounds each.Two independent pairs were run in opposite order (base→head, then head→base) so machine drift cannot explain the direction:
Every one of the 9 benchmarks was faster in both pairs. The dump-heavy configurations move the most, which matches where the cost-line count is highest relative to total runtime — e.g.
python3 testdata/test.py, no-inline: 1.360 s → 1.307 s (best-of-8, ~-3.9%).Note: the sandbox has no bare-metal
codspeed-macrorunner, so these local numbers are noisier than CI. The CI CodSpeed run on this PR is the authoritative measurement.