Erase zero-count entries in AliveVariableHash to stop unbounded memory growth#299
Erase zero-count entries in AliveVariableHash to stop unbounded memory growth#299PhilippGrulich wants to merge 3 commits into
Conversation
…y growth AliveVariableHash::decrement() left entries in the counts map at count 0, so the map grew with every value ref ever seen during tracing rather than with the number of currently-alive variables. Because the trace contexts are thread_local and reset() skipped clearing when the hash was 0 (exactly the state a balanced trace ends in), these dead entries survived reset() and accumulated across trace iterations and across traced functions for the lifetime of the thread. Erasing entries when their count reaches zero is hash-neutral: a zero count contributes (id * HASH_MULTIPLIER) * 0 = 0 to the XOR hash, which is identical to an absent entry, so snapshot hashes — and therefore trace deduplication and generated IR — are unchanged. Also make reset() check map emptiness instead of the hash, which removes the latent edge case where live entries XOR-colliding to hash 0 would leak reference counts into the next trace iteration. Add a size() accessor and unit tests covering erasure, hash-neutrality, and reset behavior in a new tracing-tests suite. https://claude.ai/code/session_01XTX9MjHW7Bgq2uSrFMXhiX
There was a problem hiding this comment.
Tracing Benchmark
Details
| Benchmark suite | Current: be70241 | Previous: 29169c1 | Ratio |
|---|---|---|---|
e2e_tiered_bc_to_mlir |
47.1988 us (± 14.3019) |
55.1658 us (± 6.30718) |
0.86 |
e2e_single_mlir |
6.17471 ms (± 145.981) |
5.4825 ms (± 39.3956) |
1.13 |
exec_mlir_add |
11.0244 ns (± 0.850019) |
10.4362 ns (± 1.32411) |
1.06 |
exec_mlir_fibonacci |
17.4523 us (± 2.33915) |
13.5485 us (± 1.81336) |
1.29 |
exec_mlir_sum |
570.932 us (± 71.3088) |
522.524 us (± 48.1743) |
1.09 |
exec_cpp_add |
4.70345 ns (± 0.818755) |
4.58975 ns (± 0.395191) |
1.02 |
exec_cpp_fibonacci |
108.138 us (± 6.31198) |
96.737 us (± 8.32178) |
1.12 |
exec_cpp_sum |
23.559 ms (± 132.158) |
36.0318 ms (± 954.073) |
0.65 |
exec_bc_add |
43.6556 ns (± 2.85635) |
45.1657 ns (± 6.22151) |
0.97 |
exec_bc_fibonacci |
697.127 us (± 9.20888) |
859.644 us (± 12.2932) |
0.81 |
exec_bc_sum |
166.011 ms (± 1.58582) |
185.87 ms (± 290.066) |
0.89 |
exec_asmjit_add |
3.8887 ns (± 0.408682) |
3.48198 ns (± 0.381308) |
1.12 |
exec_asmjit_fibonacci |
22.1294 us (± 1.34786) |
20.4936 us (± 1.61583) |
1.08 |
exec_asmjit_sum |
5.27809 ms (± 17.7377) |
4.84875 ms (± 83.9371) |
1.09 |
exec_bc_add_noRegAlloc |
44.7279 ns (± 3.51887) |
43.7698 ns (± 6.5799) |
1.02 |
exec_bc_add_regAlloc |
44.334 ns (± 4.70855) |
43.021 ns (± 3.42861) |
1.03 |
exec_bc_fibonacci_noRegAlloc |
701.288 us (± 14.2459) |
860.636 us (± 14.3992) |
0.81 |
exec_bc_fibonacci_regAlloc |
696.736 us (± 10.3021) |
859.648 us (± 13.3564) |
0.81 |
exec_bc_sum_noRegAlloc |
166.595 ms (± 849.738) |
185.984 ms (± 1.1352) |
0.90 |
exec_bc_sum_regAlloc |
165.99 ms (± 1.18093) |
185.796 ms (± 174.332) |
0.89 |
trace_add |
2.34754 us (± 209.741) |
2.66868 us (± 737.531) |
0.88 |
completing_trace_add |
2.38539 us (± 310.534) |
2.31745 us (± 209.529) |
1.03 |
trace_ifThenElse |
8.46272 us (± 1.41621) |
8.68211 us (± 645.797) |
0.97 |
completing_trace_ifThenElse |
4.38496 us (± 480.056) |
4.78687 us (± 389.14) |
0.92 |
trace_deeplyNestedIfElse |
25.724 us (± 3.63137) |
26.7948 us (± 4.11582) |
0.96 |
completing_trace_deeplyNestedIfElse |
16.5105 us (± 6.99305) |
13.1955 us (± 1.10408) |
1.25 |
trace_loop |
8.41461 us (± 1.73064) |
8.64268 us (± 859.322) |
0.97 |
completing_trace_loop |
4.59225 us (± 777.01) |
4.85343 us (± 394.846) |
0.95 |
trace_ifInsideLoop |
16.4483 us (± 3.17512) |
16.8221 us (± 1.29324) |
0.98 |
completing_trace_ifInsideLoop |
7.99677 us (± 1.12475) |
8.81194 us (± 776.945) |
0.91 |
trace_loopDirectCall |
8.43092 us (± 1.58945) |
8.74413 us (± 701.252) |
0.96 |
completing_trace_loopDirectCall |
4.58948 us (± 569.736) |
4.86294 us (± 388.782) |
0.94 |
trace_pointerLoop |
13.8356 us (± 2.52457) |
14.54 us (± 1.25771) |
0.95 |
completing_trace_pointerLoop |
9.69777 us (± 1.3246) |
10.7372 us (± 1.19379) |
0.90 |
trace_staticLoop |
7.62658 us (± 781.089) |
7.59941 us (± 762.036) |
1.00 |
completing_trace_staticLoop |
7.57709 us (± 707.368) |
7.58571 us (± 752.978) |
1.00 |
trace_fibonacci |
9.67017 us (± 1.51137) |
10.0541 us (± 954.429) |
0.96 |
completing_trace_fibonacci |
5.7895 us (± 714.623) |
6.33859 us (± 538.529) |
0.91 |
trace_gcd |
7.68103 us (± 1.12128) |
8.14535 us (± 1.33902) |
0.94 |
completing_trace_gcd |
3.85413 us (± 482.794) |
3.92369 us (± 244.195) |
0.98 |
trace_nestedIf10 |
42.2366 us (± 7.49469) |
38.2283 us (± 3.55829) |
1.10 |
completing_trace_nestedIf10 |
41.3916 us (± 6.34458) |
38.4494 us (± 3.07042) |
1.08 |
trace_nestedIf100 |
1.72323 ms (± 28.8194) |
1.35271 ms (± 34.2033) |
1.27 |
completing_trace_nestedIf100 |
1.72852 ms (± 32.0263) |
1.35886 ms (± 26.2137) |
1.27 |
trace_chainedIf10 |
101.457 us (± 9.37078) |
97.8326 us (± 5.78113) |
1.04 |
completing_trace_chainedIf10 |
46.3193 us (± 8.61204) |
49.1036 us (± 3.98698) |
0.94 |
trace_chainedIf100 |
4.92713 ms (± 62.7288) |
4.41378 ms (± 42.2565) |
1.12 |
completing_trace_chainedIf100 |
1.96641 ms (± 28.7219) |
2.33498 ms (± 350.939) |
0.84 |
tiered_compile_addOne |
48.6397 us (± 16.982) |
48.7863 us (± 6.5092) |
1.00 |
single_compile_mlir_addOne |
3.58841 ms (± 127.696) |
3.22397 ms (± 33.7542) |
1.11 |
single_compile_cpp_addOne |
28.3112 ms (± 598.266) |
||
single_compile_bc_addOne |
47.2525 us (± 15.3958) |
||
tiered_compile_sumLoop |
63.5827 us (± 17.5958) |
||
single_compile_mlir_sumLoop |
5.65635 ms (± 150.867) |
||
single_compile_cpp_sumLoop |
28.9054 ms (± 680.952) |
||
single_compile_bc_sumLoop |
65.5986 us (± 19.5522) |
||
ssa_add |
167.405 ns (± 9.50303) |
180.619 ns (± 16.2991) |
0.93 |
ssa_ifThenElse |
408.513 ns (± 25.4026) |
442.772 ns (± 21.7696) |
0.92 |
ssa_deeplyNestedIfElse |
1.06197 us (± 65.3901) |
1.14705 us (± 103.774) |
0.93 |
ssa_loop |
430.895 ns (± 24.3611) |
481.822 ns (± 39.2086) |
0.89 |
ssa_ifInsideLoop |
824.938 ns (± 49.3184) |
892.388 ns (± 62.4313) |
0.92 |
ssa_loopDirectCall |
440.186 ns (± 27.4267) |
504.737 ns (± 47.1325) |
0.87 |
ssa_pointerLoop |
514.868 ns (± 17.2975) |
580.959 ns (± 43.6345) |
0.89 |
ssa_staticLoop |
391.955 ns (± 19.0582) |
417.79 ns (± 22.7853) |
0.94 |
ssa_fibonacci |
451.462 ns (± 29.2895) |
505.716 ns (± 38.6623) |
0.89 |
ssa_gcd |
402.083 ns (± 20.068) |
457.524 ns (± 42.219) |
0.88 |
ir_add |
708.99 ns (± 34.0359) |
744.104 ns (± 65.8103) |
0.95 |
ir_ifThenElse |
1.50911 us (± 101.739) |
1.5327 us (± 131.391) |
0.98 |
ir_deeplyNestedIfElse |
3.32884 us (± 176.891) |
3.34531 us (± 197.901) |
1.00 |
ir_loop |
1.62328 us (± 91.4398) |
1.59364 us (± 107.536) |
1.02 |
ir_ifInsideLoop |
2.75266 us (± 183.573) |
2.74643 us (± 147.409) |
1.00 |
ir_loopDirectCall |
1.78082 us (± 86.968) |
1.7656 us (± 251.012) |
1.01 |
ir_pointerLoop |
1.99544 us (± 133.937) |
1.93868 us (± 167.91) |
1.03 |
ir_staticLoop |
1.41649 us (± 63.1779) |
1.43521 us (± 91.9248) |
0.99 |
ir_fibonacci |
1.71232 us (± 111.401) |
1.67468 us (± 105.804) |
1.02 |
ir_gcd |
1.46772 us (± 101.965) |
1.44252 us (± 106.028) |
1.02 |
ir_nestedIf10 |
7.75239 us (± 774.944) |
7.61239 us (± 634.1) |
1.02 |
ir_nestedIf100 |
94.6282 us (± 4.23035) |
91.815 us (± 6.54672) |
1.03 |
ir_chainedIf10 |
11.9168 us (± 1.00443) |
11.8675 us (± 1.16995) |
1.00 |
ir_chainedIf100 |
171.878 us (± 6.23946) |
171.755 us (± 9.06447) |
1.00 |
comp_mlir_add |
6.17822 ms (± 145.645) |
5.49606 ms (± 47.8474) |
1.12 |
comp_mlir_ifThenElse |
6.93105 ms (± 169.979) |
6.08652 ms (± 58.2961) |
1.14 |
comp_mlir_deeplyNestedIfElse |
5.73786 ms (± 365.227) |
5.01609 ms (± 37.782) |
1.14 |
comp_mlir_loop |
7.95148 ms (± 186.333) |
7.08689 ms (± 140.763) |
1.12 |
comp_mlir_ifInsideLoop |
30.4758 ms (± 775.423) |
28.6025 ms (± 500.389) |
1.07 |
comp_mlir_loopDirectCall |
12.8078 ms (± 193.961) |
11.9495 ms (± 368.076) |
1.07 |
comp_mlir_pointerLoop |
29.705 ms (± 604.433) |
28.1085 ms (± 1.5334) |
1.06 |
comp_mlir_staticLoop |
5.67435 ms (± 162.277) |
4.98048 ms (± 123.154) |
1.14 |
comp_mlir_fibonacci |
11.8661 ms (± 310.363) |
10.3777 ms (± 56.6437) |
1.14 |
comp_mlir_gcd |
10.7681 ms (± 1.37106) |
9.31178 ms (± 42.4221) |
1.16 |
comp_mlir_nestedIf10 |
11.2592 ms (± 187.879) |
10.3995 ms (± 68.0859) |
1.08 |
comp_mlir_nestedIf100 |
26.5883 ms (± 436.626) |
24.9594 ms (± 327.099) |
1.07 |
comp_mlir_chainedIf10 |
11.1365 ms (± 372.529) |
9.48984 ms (± 153.327) |
1.17 |
comp_mlir_chainedIf100 |
22.5668 ms (± 842.655) |
20.0433 ms (± 366.706) |
1.13 |
comp_cpp_add |
27.876 ms (± 688.13) |
24.4821 ms (± 305.668) |
1.14 |
comp_cpp_ifThenElse |
28.8895 ms (± 1.22864) |
25.2276 ms (± 453.104) |
1.15 |
comp_cpp_deeplyNestedIfElse |
29.4051 ms (± 1.28613) |
26.6526 ms (± 231.52) |
1.10 |
comp_cpp_loop |
28.2015 ms (± 739.819) |
25.6528 ms (± 389.363) |
1.10 |
comp_cpp_ifInsideLoop |
28.9378 ms (± 486.405) |
26.0974 ms (± 391.546) |
1.11 |
comp_cpp_loopDirectCall |
28.0722 ms (± 332.109) |
25.5574 ms (± 346.232) |
1.10 |
comp_cpp_pointerLoop |
28.9353 ms (± 661.293) |
25.8688 ms (± 717.934) |
1.12 |
comp_cpp_staticLoop |
28.2561 ms (± 744.675) |
24.9136 ms (± 542.19) |
1.13 |
comp_cpp_fibonacci |
28.0926 ms (± 911.659) |
25.3326 ms (± 365.229) |
1.11 |
comp_cpp_gcd |
28.1793 ms (± 1.42824) |
25.119 ms (± 326.674) |
1.12 |
comp_cpp_nestedIf10 |
30.7194 ms (± 641.675) |
28.1346 ms (± 513.99) |
1.09 |
comp_cpp_nestedIf100 |
65.6357 ms (± 2.641) |
61.36 ms (± 505.878) |
1.07 |
comp_cpp_chainedIf10 |
33.121 ms (± 426.856) |
30.6378 ms (± 1.4452) |
1.08 |
comp_cpp_chainedIf100 |
94.2024 ms (± 599.955) |
90.6793 ms (± 1.04642) |
1.04 |
comp_bc_add |
38.3425 us (± 18.3063) |
14.5105 us (± 1.83749) |
2.64 |
comp_bc_ifThenElse |
16.8031 us (± 3.47064) |
18.364 us (± 2.43447) |
0.92 |
comp_bc_deeplyNestedIfElse |
20.7125 us (± 4.30445) |
22.3192 us (± 2.64468) |
0.93 |
comp_bc_loop |
17.0039 us (± 3.65436) |
18.3371 us (± 2.50255) |
0.93 |
comp_bc_ifInsideLoop |
19.2778 us (± 4.17949) |
20.9826 us (± 2.36883) |
0.92 |
comp_bc_loopDirectCall |
17.1364 us (± 3.89527) |
19.3295 us (± 3.73433) |
0.89 |
comp_bc_pointerLoop |
18.22 us (± 3.85365) |
20.1003 us (± 3.55418) |
0.91 |
comp_bc_staticLoop |
16.6354 us (± 3.52371) |
17.087 us (± 3.46154) |
0.97 |
comp_bc_fibonacci |
17.2323 us (± 3.31152) |
18.544 us (± 2.37258) |
0.93 |
comp_bc_gcd |
16.4919 us (± 3.43437) |
18.2354 us (± 2.55259) |
0.90 |
comp_bc_nestedIf10 |
31.9254 us (± 5.57274) |
35.3054 us (± 4.74569) |
0.90 |
comp_bc_nestedIf100 |
196.433 us (± 12.3475) |
195.654 us (± 11.1705) |
1.00 |
comp_bc_chainedIf10 |
43.3055 us (± 7.4848) |
50.2381 us (± 6.12895) |
0.86 |
comp_bc_chainedIf100 |
309.991 us (± 9.68734) |
299.098 us (± 11.1722) |
1.04 |
comp_asmjit_add |
18.2076 us (± 5.1078) |
21.2607 us (± 3.25419) |
0.86 |
comp_asmjit_ifThenElse |
27.1376 us (± 5.93483) |
32.7678 us (± 4.30135) |
0.83 |
comp_asmjit_deeplyNestedIfElse |
49.6131 us (± 14.6538) |
55.1126 us (± 6.58445) |
0.90 |
comp_asmjit_loop |
28.9518 us (± 5.28427) |
35.2551 us (± 4.7251) |
0.82 |
comp_asmjit_ifInsideLoop |
48.7946 us (± 11.2784) |
55.9642 us (± 7.09687) |
0.87 |
comp_asmjit_loopDirectCall |
32.5802 us (± 5.73255) |
46.0845 us (± 9.03972) |
0.71 |
comp_asmjit_pointerLoop |
34.6505 us (± 6.77813) |
48.2887 us (± 7.376) |
0.72 |
comp_asmjit_staticLoop |
24.3843 us (± 4.80616) |
27.798 us (± 2.74827) |
0.88 |
comp_asmjit_fibonacci |
31.0006 us (± 5.77307) |
42.2674 us (± 5.11804) |
0.73 |
comp_asmjit_gcd |
29.8742 us (± 6.83298) |
34.9165 us (± 4.37222) |
0.86 |
comp_asmjit_nestedIf10 |
97.0275 us (± 13.5331) |
101.543 us (± 9.46494) |
0.96 |
comp_asmjit_nestedIf100 |
1.04407 ms (± 15.0573) |
1.03108 ms (± 19.1126) |
1.01 |
comp_asmjit_chainedIf10 |
149.758 us (± 14.1057) |
151.582 us (± 12.3314) |
0.99 |
comp_asmjit_chainedIf100 |
2.20803 ms (± 36.3203) |
2.13293 ms (± 21.8006) |
1.04 |
exec_bc_addOne |
35.3675 ns (± 2.1834) |
36.0102 ns (± 5.86563) |
0.98 |
exec_mlir_addOne |
273.286 ns (± 5.78411) |
257.224 ns (± 2.57444) |
1.06 |
exec_cpp_addOne |
3.72856 ns (± 0.285566) |
3.61316 ns (± 0.437555) |
1.03 |
exec_interpreted_addOne |
38.381 ns (± 1.75678) |
37.0801 ns (± 1.84741) |
1.04 |
This comment was automatically generated by workflow using github-action-benchmark.
Value refs are assigned monotonically and never reused, so during tracing the map sees a stream of ids that are incremented once and decremented to zero shortly after. Verify that this churn neither perturbs the hash nor grows the map while a long-lived entry survives it. https://claude.ai/code/session_01XTX9MjHW7Bgq2uSrFMXhiX
|
A note on the
I reproduced this locally (same machine, back-to-back builds,
I also tested deferring reclamation to a batched sweep to avoid the per-lifetime node alloc/free; it was slower than eager erase on the real benchmark (erased nodes are immediately recycled by the allocator for the next insert, and the table stays minimal), so eager erase stays. End-to-end impact is small because tracing is a thin slice of compilation: Generated by Claude Code |
…speed Erasing zero-count entries bounds the map to O(alive variables) but routes one malloc/free pair per traced value through the general-purpose allocator, which slowed tracing of large functions by up to ~1.3x. Replace the default allocator with an internal pool that hands map nodes out of bump-allocated 16KB chunks and recycles freed nodes via per-size free lists (the allocator is rebound to different types - nodes and bucket arrays - so size classes must not be mixed). This recovers most of the regression: the tracing benchmark suite runs within ~4% of the pre-fix baseline (down from ~17% with plain erasure) while the pool retains a single 16KB chunk per trace context across the entire suite. A bump-only arena would not fit this workload: without per-node reclamation, the insert/erase churn would regrow memory with every value ref ever seen - the leak this fix removes. https://claude.ai/code/session_01XTX9MjHW7Bgq2uSrFMXhiX
|
Follow-up on the tracing regression: The regression was dominated by the malloc/free pair per traced value that eager erasure introduced. A bump-only arena (like Local numbers (same machine, back-to-back; full tracing suite):
Several benchmarks now beat the old baseline ( Memory stays bounded: instrumentation shows the pool retains a single 16 KB chunk per trace context across the entire benchmark suite, versus the old code retaining one map entry per value ref ever seen (≈400 MB for a 10M-ref path-explosion trace, never freed). Generated by Claude Code |
Problem
AliveVariableHashtracks reference counts of alive value refs and feeds an incrementally maintained XOR hash intoSnapshot, which keys the tag maps used for trace deduplication. Two behaviors combined into unbounded memory growth:decrement()never removed entries. When a value ref's count dropped to 0, the entry stayed in theunordered_map. Value refs are assigned monotonically and never reused (lastValueRefis not reset inresetExecution()), so the map grew with every value ref ever seen across all symbolic-execution iterations, not with the number of currently-alive values. Path-explosion traces accumulate millions of entries in a singletrace()call.reset()skipped clearing when the hash was 0 — which is exactly the state a balanced trace iteration ends in (all counts back to 0 ⇒ hash back to 0). So the dead entries survivedreset().Because both trace contexts (
ExceptionBasedTraceContext,LazyTraceContext) arethread_localstatics, the high-water-mark map persisted for the lifetime of the thread. Standalone measurement of the pathological case: 10M unique value refs retain ~400 MB thatreset()never frees; with the fix the structure stays at O(alive variables).Fix
decrement()erases the entry when the count reaches 0 (commit 1). This is hash-neutral: a zero count contributes(id * HASH_MULTIPLIER) * 0 = 0to the XOR hash, identical to an absent entry — verified op-by-op in simulation and by unit test. Snapshot hashes, trace deduplication, and generated IR are bit-identical.reset()checks map emptiness instead ofhash != 0(commit 1). Also removes a latent edge case: live entries XOR-colliding to hash 0 would previously skip the clear and leak reference counts into the next trace iteration.Performance (local, back-to-back builds, full tracing suite)
completing_trace_nestedIf100)Several benchmarks beat the old baseline (
trace_deeplyNestedIfElse0.85,completing_trace_chainedIf1000.90). End-to-end compilation impact is ~1.02–1.04 since tracing is a thin slice of it.Testing
AliveVariableHashunit tests in a newtracing-testssuite covering erasure, hash-neutrality, monotonic-id churn, count sensitivity, and reset.https://claude.ai/code/session_01XTX9MjHW7Bgq2uSrFMXhiX