Skip to content

Specialize membership index by concrete key type - #2

Merged
tannevaled merged 1 commit into
mainfrom
specialized-hash-index
Jul 3, 2026
Merged

tannevaled merged 1 commit into
mainfrom
specialized-hash-index

Conversation

@tannevaled

Copy link
Copy Markdown
Contributor

What

The membership index was a single map[any]struct{}. A generic-any map hashes every key through the interface's type descriptor — roughly 2x the cost of a map keyed by a concrete comparable type — and that interface hashing was the dominant residual cost on build and union, the two ops still losing to MRI+YJIT.

Most real Sets are homogeneous, so the index now keeps a concrete-typed map for the common key types (all Go int, all int64, all string) and falls back to the generic map only for mixed / other-typed sets. A new membership type carries a kind discriminator plus the active map; the hot paths (has/add/putNew/del) are a single predictable branch on kind, then a concrete-typed map op. clone() uses maps.Clone on the active typed map, preserving Union's fast bulk copy.

Exact Ruby semantics preserved

Each fast path is gated on one concrete type, so int(1), int64(1), 1.0 and "1" remain four distinct members. The first key of a foreign concrete type promotes the whole index to the generic map, re-boxing every existing key under its original type so nothing is lost or coalesced. Insertion order, dedup, delete, the full algebra/predicate surface and the MRI oracle are unchanged.

Measured (Apple M4 Max, ruby 4.0.5 +PRISM / +YJIT, best-of, ns/op)

op before (vs MRI) after (vs MRI) after (vs YJIT) verdict
build-1000 1.87x 1.35x 1.33x honest floor (order slices)
union-1000 1.57x 1.07x 1.06x parity
intersection-1000 1.11x 0.78x 0.82x beats YJIT
membership-1000 0.41x 0.28x 0.74x beats YJIT

The build/union residual is the two parallel insertion-order slices Go must maintain because Go maps are not insertion-ordered — work MRI's ordered Hash provides for free (the typed-map dedup alone is already ~MRI parity). intersection and membership now beat both MRI and YJIT.

Tests

Homogeneous fast paths (int/int64/string), generic fallback, every type migration (including a set that starts homogeneous then gains a mixed-type member), order preservation across migration, delete/clone/putNew per kind. 100% statement coverage; -race clean; MRI oracle green.

🤖 Generated with Claude Code

The membership index was a single map[any]struct{}. A generic-any map hashes
every key through the interface's type descriptor — roughly twice the cost of a
map keyed by a concrete comparable type — and that interface hashing was the
dominant residual cost on build and union, the two ops still losing to MRI+YJIT.

Most real Sets are homogeneous, so the index now keeps a concrete-typed map for
the common key types (all Go int, all int64, all string) and falls back to the
generic map only for mixed / other-typed sets. The new membership type carries a
kind discriminator plus the active map; hot paths (has / add / putNew / del) are
a single predictable branch on kind, then a concrete-typed map op. clone() uses
maps.Clone on the active typed map, keeping Union's fast bulk copy.

Exact Ruby semantics are preserved. Each fast path is gated on ONE concrete type,
so int(1), int64(1), 1.0 and "1" remain four distinct members: the first key of a
foreign concrete type promotes the whole index to the generic map, re-boxing every
existing key under its original type so nothing is lost or coalesced. Insertion
order, dedup, delete, the full algebra/predicate surface and the MRI oracle are
unchanged.

Measured (Apple M4 Max, ruby 4.0.5 +PRISM/+YJIT, best-of, ns/op):

  op            before(vsMRI)  after(vsMRI)  after(vsYJIT)
  build-1000        1.87x         1.35x         1.33x  (floor: order slices)
  union-1000        1.57x         1.07x         1.06x  (parity)
  intersection      1.11x         0.78x         0.82x  (beats YJIT)
  membership        0.41x         0.28x         0.74x  (beats YJIT)

build/union residual is the two parallel insertion-order slices Go must maintain
because Go maps are not insertion-ordered — work MRI's ordered Hash provides for
free (the typed map dedup alone is already ~MRI parity). intersection and
membership now beat both MRI and YJIT.

Tests: homogeneous fast paths (int/int64/string), generic fallback, every type
migration (incl. a set that starts homogeneous then gains a mixed-type member),
order preservation across migration, delete/clone/putNew per kind. 100% statement
coverage; race-clean; MRI oracle green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@tannevaled
tannevaled merged commit 246794d into main Jul 3, 2026
9 checks passed
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