Skip to content

Honor user-defined hash/eql? for Hash keys and add compare_by_identity#194

Merged
hadashiA merged 1 commit into
mainfrom
claude/hash-key-comparer
Jul 15, 2026
Merged

Honor user-defined hash/eql? for Hash keys and add compare_by_identity#194
hadashiA merged 1 commit into
mainfrom
claude/hash-key-comparer

Conversation

@hadashiA

Copy link
Copy Markdown
Owner

Summary

Fixes two mruby/CRuby-incompatible behaviors in Hash key handling, mirroring mruby's ht_hash_value / ht_hash_equal:

  1. User-defined #hash was ignored for bucketing. A key whose class overrides hash/eql? could not be looked up via an eql?-equal instance — MRubyValueHashKeyEqualityComparer.GetHashCode used the CLR identity hash unconditionally. Now object keys dispatch #hash (immediates by value, strings by content, exactly like mruby). The default Kernel#hash is detected via the method cache and short-circuited, so identity-keyed hashes don't pay a Send per operation.
  2. Hash#compare_by_identity / compare_by_identity? implemented (previously missing). This turned out to be a hard requirement for fix 1: optcarrot's PPU keys @lut_update by mutable nametable arrays and calls compare_by_identity. On mruby it gets a real object_id-keyed implementation from optcarrot's tools/shim.rb; ChibiRuby's benchmark prelude had a no-op that only worked because the old comparer was accidentally identity-based. With correct content semantics and no real compare_by_identity, optcarrot renders garbage (checksum 6730 vs the mruby-verified 59662).

Supporting changes:

  • Array#hash implemented in C# (same 12347 / __update_hash algorithm as Enumerable#hash, Integer fast path). With #hash now honored, array keys were probing through the Ruby-implemented Enumerable#hash; optcarrot's setup_lut (~65k array-key probes) regressed +130ms until this.
  • Mutation guard in RHash probes: a user #hash/#eql? that mutates the hash mid-probe (upstream hash.rb tests this) now retries once instead of surfacing .NET Dictionary's concurrent-modification exception; Rehash clamps its trim similarly. (mruby raises "hash modified" here; result is unspecified but must not crash.)
  • Comparer hot entries are kept tiny with NoInlining slow tails; identity mode is a flag on the same comparer class so Dictionary probe sites stay monomorphic for GDV.

Numbers (order-alternated pairs)

benchmark before after
string-key Hash#[] micro ~62 ms ~55 ms (−11%) (no more Send(:eql?) per probe)
symbol-key Hash#[] micro parity
bm_ao_render ~−1%
optcarrot 24.2 fps 22.9 fps (−5%), checksum 59662

The optcarrot −5% is a known open item: profiling shows the hash path is cold in both builds, and the delta survives removing the mutation guard, unifying/splitting the comparer class, dropping the added state field, and inert method reorders — pointing at a JIT compile-order/code-placement effect rather than executed-code cost. Investigation continues; the correctness fix is submitted on its own merits (the old behavior silently loses hash entries for custom keys).

Tests

157 passed, including new specs for cross-instance custom-key lookup, compare_by_identity (incl. the mutated-array-key pattern optcarrot relies on), and hash/eql? collision cases. optcarrot video checksum verified identical to mruby-original ground truth (59662).

🤖 Generated with Claude Code

@hadashiA

Copy link
Copy Markdown
Owner Author

The optcarrot −5% is explained — it is not a comparer cost

Instrumented builds show the comparer does essentially zero work per frame (steady state: ~14 identity-fast-path probes/frame, 0 Send(:hash)/Send(:eql?) — all 100k dispatches happen once during PPU setup_lut). A dead-code build (all new code present, old semantics executed) is at parity with main, and so is a build with correct semantics where only optcarrot's entries cache is forced to identity keys:

build optcarrot fps
main (broken key semantics) 24.2–24.4
this PR 22.9 (−5%)
this PR + entries = {}.compare_by_identity in setup_lut 24.2–24.3 (parity)

The whole delta comes from setup_lut's entries[key] ||= ... dedup cache actually working now: with correct content-keyed semantics, @attr_lut's 32k slots share ~few-thousand entry arrays (as the optcarrot author intended, and as mruby/CRuby build it); with main's broken identity semantics every slot got its own freshly-allocated 3-element array, which happens to lay out contiguously in allocation order and renders slightly faster on this machine.

In other words, main's 24.2 fps included an accidental data-layout bonus from the Hash bug; 22.9 fps is optcarrot running on the same data shape that mruby (39.4 fps, checksum 59662) runs. The comparison with mruby is now apples-to-apples.

🤖 Generated with Claude Code

@hadashiA
hadashiA merged commit 473e2a7 into main Jul 15, 2026
1 check passed
@hadashiA
hadashiA deleted the claude/hash-key-comparer branch July 15, 2026 00:12
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