Skip to content

fix(dict): dict_remove no longer inflates the hash table exponentially#531

Merged
InauguralPhysicist merged 1 commit into
mainfrom
dict-remove-rebuild-inflation
Jul 10, 2026
Merged

fix(dict): dict_remove no longer inflates the hash table exponentially#531
InauguralPhysicist merged 1 commit into
mainfrom
dict-remove-rebuild-inflation

Conversation

@InauguralPhysicist

Copy link
Copy Markdown
Collaborator

The bug

env_hash_rebuild unconditionally doubled the old capacity. Every grow-path caller rebuilds at >70% load, where doubling is correct — but dict_remove rebuilds merely to re-index after a removal (indices shift down), and blind doubling there inflates a shrinking dict's table by 2^N over N removes.

Minimal repro (aborts OOM at ~25 iterations pre-fix, 128MB+ single allocation):

d is {}
i is 0
loop while i < 200:
    d["k"] is 1
    dict_remove of [d, "k"]
    i is i + 1

This is also what repeatedly took down the dev box today: the #523 liferaft task-layer migration does one dict_remove per delivered message on a small registry dict — the first workload to ever churn dict_remove on a long-lived dict. Root-caused via gdb on the malloc-failure abort under ulimit -v (backtrace: x_oom ← xcalloc ← env_hash_rebuild ← builtin_dict_remove).

The fix

Size the rebuilt table from the live entry count (new_cap = next_pow2(max(count*2, ENV_HASH_INIT_CAP))). At the >70%-load grow sites count*2 lands on exactly the old doubling; at the remove site the table now tracks the dict's actual size.

Tests

tests/test_dict.eigs: 200-cycle single-key churn (impossible pre-fix, instant post-fix) + a churn-with-survivors check pinning lookup correctness across re-index rebuilds.

Validation

  • Full suite, release: 2703/2703
  • Full suite, ASan + detect_leaks=1: 2703/2703, leak tally 0

Related: #530 (the second gap the same migration surfaced — task handle reaping).

🤖 Generated with Claude Code

env_hash_rebuild blindly doubled the old capacity — right for the
grow-on-insert callers (all rebuild at >70% load), catastrophic for
dict_remove's re-index rebuild: N removes on one dict grew its table
by 2^N, so ~25 insert/remove cycles of a single key allocated
gigabytes and aborted OOM. Size the new table from the live entry
count instead — identical doubling behavior at the grow sites, no
inflation on remove.

Surfaced by liferaft's per-message pending-registry churn during the
#523 task-layer migration; regression-pinned with a 200-cycle churn
test (impossible pre-fix, instant post-fix) plus a survivors-resolve
check in tests/test_dict.eigs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@InauguralPhysicist InauguralPhysicist merged commit 3a09fbd into main Jul 10, 2026
17 checks passed
@InauguralPhysicist InauguralPhysicist deleted the dict-remove-rebuild-inflation branch July 10, 2026 02:09
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