Skip to content

perf: add/delete churn on a small object is ~33x node — shape identity turns over on EVERY op (and the bench's own header now argues the opposite of reality) #9065

Description

@proggeramlug

The number

bench_dynamic_property_keys.ts's delete-heavy loop: perry 981 ms vs node 30 ms (Mac mini, min-of-7, current main with #9038 default-on). Cross-engine: scriptc 29 ms (node parity), porffor fails to compile it. Sibling issue for the large-object case: #9064 — same root cause, different scale and different dominant term.

Self-contained repro

function deleteHeavy(n: number): number {
  const o: Record<string, number> = {};
  let s = 0;
  for (let i = 0; i < n; i++) {
    const k = "k" + (i % 500);
    o[k] = i;
    s += o[k];
    delete o[k]; // the object never holds more than one live key
  }
  return s;
}
// N = 300_000 → perry ~981 ms, node ~30 ms.

Unlike #9064's stable 500-key receiver, this object oscillates around one live key. Per iteration: an append (new shape id), a read (IC primed against it), a delete (successor id minted, every cache retired). ~3.3 µs/op vs node's ~0.1 µs — the shape-identity machinery IS the loop.

What tombstones already do here (and why it's not enough)

The tombstone threshold (key_count >= 16 before squeezing) means the array grows to 16 slots, squeezes back to ~1, repeats — amortized ~1 slot-copy per delete. Fine. What remains is that all three ops per iteration change or chase shape identity:

  • the append publishes a successor id (lineage mint + facts-index insert),
  • the delete publishes another one + sweeps stale ids for the owned address,
  • and both retirements force the read's and write's ICs to miss every single iteration (js_put_value_set_dyn_ic_miss and the get_field_by_name tail dominate the old profile of this binary, and the descriptor mint/lookup pair — shape_descriptor_by_id + ensure_with_holes — runs multiple times per op).

What to optimize, in order

  1. Stable-token deletes + per-key IC validationperf: populated delete is ~14x node after tombstones — the residual is IC retirement per delete, not the delete itself #9064's option (1). For THIS bench it removes the delete-side retirement; the read IC then survives the delete and only the append still moves identity.
  2. Append-side identity churn on re-add: a re-added key appends and mints an id per append. For churn shapes, a small transition cache keyed (predecessor_id, key) → successor id already exists for first-time builds — verify it HITS on the delete→re-add cycle (the tombstone delete's fresh generation may make every re-add transition a cold miss forever). If each k(i%500) cycle re-mints instead of reusing 500 cached transitions, that's the memory-and-time leak to close.
  3. Small-object fast path: below the 16-key threshold the per-delete publish could collapse to hole-write + epoch bump with NO descriptor machinery at all once (1) lands — a one-live-key object needs none of it.

Housekeeping in the same change

The bench's header comment (dated 2026-08-28) now argues the inverse of reality and should be rewritten when this is touched: it says the delete penalty is 1.1× ('LOWER than node's — dictionary mode refuted') and overwrite is the real 60× gap. After the write campaigns, overwrite is 13 ms and beats node (19 ms); the delete penalty is 75×. The ratio that used to refute the dictionary-mode argument is now the strongest evidence for #9064's per-key-invalidation work.

Acceptance

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions