perf(callgrind): replace modular division with power-of-2 bitwise AND in hash tables#29
Conversation
Greptile SummaryReworks Callgrind’s BBCC, context, and JCC hash tables to use power-of-two capacities and bitmask-based indexing.
Confidence Score: 5/5The PR appears safe to merge because no eligible blocking failure remains. No blocking failure remains.
|
| Filename | Overview |
|---|---|
| callgrind/bbcc.c | Uses a larger power-of-two BBCC table, exact doubling, and mixed pointer hashes with masked bucket selection. |
| callgrind/context.c | Converts the context table to power-of-two sizing and replaces modulo-based indexing with equivalent masking. |
| callgrind/global.h | Adds the shared inline hash finalization helper used by the modified pointer-based tables. |
| callgrind/jumps.c | Uses a power-of-two JCC table and mixed, masked indexing across lookup, insertion, and rehashing. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart LR
K[Hash-table key] --> H[Compute hash]
H --> F[Fold upper bits where applicable]
F --> M[Mask with size - 1]
M --> B[Select bucket]
B --> L[Traverse collision chain]
L --> R{Resize above threshold?}
R -- Yes --> D[Double table size]
D --> X[Rehash entries]
X --> B
R -- No --> E[Continue lookup or insertion]
Reviews (5): Last reviewed commit: "perf(callgrind): extract shared hash fin..." | Re-trigger Greptile
Merging this PR will improve performance by 10.88%
Performance Changes
Tip Curious why this is faster? Comment Comparing Footnotes
|
|
@codspeedbot compare the codegen of the compiler before and after. show me the assembly. i feel like it might have already be optimized to the same code |
Good question — I compiled both versions and looked at the assembly. The short answer: no, the compiler was not already producing equivalent code. The old modulo really does emit a hardware The key subtlety is that in all three hash functions, Isolated reduction (same hash input, only
|
| /* Use bit mixing instead of modular division for power-of-2 table sizes. | ||
| * Shift right to discard alignment zeros from heap pointers, use | ||
| * different shift amounts for bb and cxt to avoid cancellation on XOR, | ||
| * then fold upper bits down for better distribution in low bits. */ | ||
| UWord h = ((UWord)bb >> 4) ^ ((UWord)cxt >> 3); | ||
| h ^= h >> 16; | ||
| return (UInt)h & (size - 1); |
There was a problem hiding this comment.
Since we're using the same functionality twice, move it to a helper function and then use it here and in jumps.c
There was a problem hiding this comment.
Good call — this is already done on the current branch. I extracted the shared hash finalization step into a helper and use it in both call sites:
callgrind/global.h— newCLG_(hash_final)(UWord h, UInt size)inline helper that folds the upper bits down (h ^= h >> 16) and masks to the power-of-2 table size ((UInt)h & (size - 1)).callgrind/bbcc.c(bbcc_hash_idx) — builds the mixed value and callsCLG_(hash_final).callgrind/jumps.c(jcc_hash_idx) — same, callsCLG_(hash_final).callgrind/context.c— also switched over to the shared helper.
Each site keeps its own input-mixing (the shifts/multipliers tuned for its keys) and only the common finalization + masking is shared, so there's no behavioral change beyond the intended refactor. This landed in the commit perf(callgrind): extract shared hash finalization helper, so no further changes are needed here.
Summary
- Warming up the performance wizard...
- Handling comment — Comment handling finished successfully
Was this response helpful? React with 👍 or 👎 to help us improve CodSpeedBot.
|
@codspeedbot rebase onto latest base branch |
|
Done. I extracted the shared finalize-and-mask step into a single inline helper and used it in both hash functions.
The change is behavior-preserving — both sites produce the exact same hash values as before, just without the duplicated finalization logic. I verified the extracted logic compiles cleanly; a full Valgrind build wasn't possible in this environment since the autotools toolchain isn't available in the sandbox. Summary
Was this response helpful? React with 👍 or 👎 to help us improve CodSpeedBot. |
Done — the branch is now up to date with the latest base branch. I initially attempted a clean linear rebase onto To get you the same outcome — the latest base branch changes in this PR — without rewriting the existing commits, I merged Branch now points at If you'd specifically prefer a linear rebase rather than a merge commit, that would need a force-push, which I can't perform from here — let me know and I can outline the exact commands. Summary
Was this response helpful? React with 👍 or 👎 to help us improve CodSpeedBot. |
a2e8657 to
fa894df
Compare
Unable to generate the flame graphsThe performance report has correctly been generated, but there was an internal error while generating the flame graphs for this run. We're working on fixing the issue. Feel free to contact us on Discord or at support@codspeed.io if the issue persists. |
… in hash tables Switch the BBCC, context, and JCC hash tables to power-of-2 sizes and replace `% size` with `& (size - 1)` in all index computations. Integer division is one of the slowest ALU operations; bitwise AND is a single cycle, and setup_bbcc hits these lookups on essentially every basic block execution. - Initial sizes: BBCC 16384, context 4096, JCC 8192 - Resize formula: 2*size+3 -> 2*size to keep tables powers of two - Add bit mixing to the BBCC and JCC hash functions (shift out pointer alignment zeros, XOR-fold upper bits down) for good low-bit distribution under AND masking
Move the duplicated power-of-2 finalize-and-mask step used by bbcc_hash_idx and jcc_hash_idx into a shared CLG_(hash_final) inline helper in global.h. Behavior is unchanged; each call site keeps its own mixing formula.
fa894df to
63c7d85
Compare
Summary
Replaces expensive modular division (
%) with bitwise AND masking (& (size-1)) in all three Callgrind hash table lookups — the hottest code path in valgrind-codspeed, hit on effectively every basic block execution viasetup_bbcc.Integer division is one of the slowest ALU operations (~20–90 cycles on modern x86), while bitwise AND takes a single cycle. Making the masking valid required switching the BBCC, context, and JCC hash tables to power-of-2 sizes.
Changes
Across
callgrind/bbcc.c,callgrind/context.c, andcallgrind/jumps.c:16384(BBCC),4096(context),8192(JCC); resize formula changed from2*size+3to2*sizeso tables stay powers of two.% size→& (size - 1)in every hash-index computation (lookup, insert, and rehash-on-resize paths).Correctness
Built the local tree (VEX + coregrind + callgrind) and installed successfully — clean compile, no new warnings. Ran Callgrind on the
python3workload in both the simple (--read-inline-info=yes) and full cache-simulation configurations; both produce valid output. A/B comparison of baseline vs. optimized builds on the same workload yields identical function-collection counts (2207 functions) and equivalent profile summaries, confirming the new hash functions do not change what is collected — only how quickly entries are located.Performance
This benchmark suite measures Valgrind's own wall-clock overhead, which requires the CodSpeed walltime instrument on the
codspeed-macroCI runners. Thefullconfigurations exercise the most hash-table lookups (cache simulation) and are expected to benefit most.Note: local benchmarking in the wizard sandbox was not possible — walltime mode requires
systemd-run(unavailable here) and simulation mode cannot instrument Valgrind nested under Valgrind. The authoritative measurement is produced by this PR's CodSpeed CI run against themasterbaseline.Prior measurement of this change (CodSpeed walltime, macro runners) showed ~3.5% faster on the
fullconfigurations and +2.07% overall.