perf(VEX): stop scanning dead bindings in the HashHW guest-state environment - #34
Conversation
The guest-state environment used by the redundant Get/Put removal passes is a linear-scan map that only marked deleted bindings as unused, never shrinking. Since the environment is wiped wholesale very often (exits, dirty helpers, CAS, LLSC, MBE, AbiHint, precise-exception memory accesses), every later lookup, insert and overlap invalidation walked the dead slots as well. Keep the bindings packed in [0 .. used-1]: drop the inuse[] array, add deleteHHW() which fills the hole with the last binding, and turn the whole-environment wipes into 'used = 0'. Keys are unique among live bindings, so removing tombstones and reordering slots cannot change which binding a lookup finds.
Merging this PR will not alter performance
Comparing Footnotes
|
Greptile SummaryThe PR replaces HashHW tombstones with a packed live-entry representation, reducing optimizer scans to the number of active bindings.
Confidence Score: 5/5The PR appears safe to merge, with the packed-map invariant and swap-delete iteration handled consistently across all changed paths. Every live binding remains within the range bounded by
|
| Filename | Overview |
|---|---|
| VEX/priv/ir_opt.c | Converts the optimizer's HashHW maps from tombstoned storage to packed storage and consistently updates deletion, reset, lookup, insertion, resizing, and invalidation behavior. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[Scan live entries 0 through used-1] --> B{Entry invalidated?}
B -- No --> C[Advance index]
B -- Yes --> D[Decrement used]
D --> E[Move last live entry into current slot]
E --> A
C --> A
F[Whole environment invalidated] --> G[Set used to zero]
Reviews (1): Last reviewed commit: "VEX ir_opt: keep HashHW bindings packed ..." | Re-trigger Greptile
Problem
Profiling
valgrind --tool=callgrindon the benchmark workloads shows that IR translation is a large share of the runtime, and inside it the redundant Get/Put removal passes are unexpectedly expensive:invalidateOverlaps(~1.8% of total),redundant_get_removal_BB(~1.7%),addToHHW(~1.2%) andredundant_put_removal_BB(~0.9%), pluslookupHHWinlined into them.The cause is the
HashHWmap used as the guest-state environment inVEX/priv/ir_opt.c. It is a linear-scan map with tombstones: deleting a binding only clearsinuse[i], andusednever shrinks. The environment is wiped wholesale very often — on everyIst_Exit, on dirty helpers,CAS,LLSC,MBEandAbiHint, and at memory accesses under precise-exception control — yet each wipe left the array just as long as before. Every subsequent lookup, insert and overlap-invalidation then walked all those dead slots, on top of the extrainuse[]cache line traffic and branch per slot.Change
Keep the bindings packed in
[0 .. used-1]:inuse[]array. Every slot belowusedis live, so scans compare keys only.deleteHHW(), which fills the hole with the last binding (the map is unordered, so this is safe); iterating callers re-examine the slot.env->used = 0— O(1) instead of O(used), and, more importantly, every later scan is now proportional to the number of live bindings.invalidateOverlaps, the precise-exception flush inhandle_gets_Stmt, and the aliasing invalidation indo_cse_BBnow delete instead of tombstoning.Behaviour is unchanged: keys are unique among live bindings, so removing tombstones and reordering slots cannot change which binding a lookup finds.
Validation
Two Valgrind builds were compiled from this branch and from its parent commit (
amd64-linux,--enable-only64bit, Capstone cycle-estimation enabled) and compared.Correctness
perl tests/vg_regtest none memcheck callgrind cachegrindon the patched build: 576 tests, 25 stderr + 5 stdout failures, all confined tonone/tests(fdleak, rlimit, stackgrowth, sigstackgrowth, map_unmap, bigcode, track_*, getdents_filter, xml-track-fds). memcheck, callgrind and cachegrind pass with 0 failures, includingnone/tests/iropt-testandmemcheck/tests/vbit-test.callgrind --read-inline-info=yes echorun is identical between the two builds (samesummary:/totals:of 147 710, same call graph); the only textual difference is the build's own install path in anob=line.Performance
Measured with the repository's own harness (
bench/generate_config.py+codspeed run -m walltime), 20 measured rounds per benchmark after a 1 s warmup, over 30 Callgrind configurations (echo,python3,stress-ng --cpu 1,stress-ng --cpu 4,llsc_tzconvert_bench× 6 Callgrind configs).To rule out machine drift, the pair was run twice in opposite orders:
Per-benchmark change in the reverse-order pairing: min −2.75%, median −1.84%, max −0.47%. No benchmark regressed in either pairing.
Notes on the local measurement environment: it is a shared x86_64 sandbox rather than a
codspeed-macrorunner, so absolute numbers are noisier than CI's. Thetake_stringscommand from the CI matrix was excluded because its fixture requires a/nix/storeloader that is unavailable here; everything else in the matrix was measured.