Honor user-defined hash/eql? for Hash keys and add compare_by_identity#194
Conversation
The optcarrot −5% is explained — it is not a comparer costInstrumented builds show the comparer does essentially zero work per frame (steady state: ~14 identity-fast-path probes/frame, 0
The whole delta comes from 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 |
Summary
Fixes two mruby/CRuby-incompatible behaviors in Hash key handling, mirroring mruby's
ht_hash_value/ht_hash_equal:#hashwas ignored for bucketing. A key whose class overrideshash/eql?could not be looked up via aneql?-equal instance —MRubyValueHashKeyEqualityComparer.GetHashCodeused the CLR identity hash unconditionally. Now object keys dispatch#hash(immediates by value, strings by content, exactly like mruby). The defaultKernel#hashis detected via the method cache and short-circuited, so identity-keyed hashes don't pay a Send per operation.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_updateby mutable nametable arrays and callscompare_by_identity. On mruby it gets a real object_id-keyed implementation from optcarrot'stools/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 realcompare_by_identity, optcarrot renders garbage (checksum 6730 vs the mruby-verified 59662).Supporting changes:
Array#hashimplemented in C# (same12347/__update_hashalgorithm asEnumerable#hash, Integer fast path). With#hashnow honored, array keys were probing through the Ruby-implementedEnumerable#hash; optcarrot'ssetup_lut(~65k array-key probes) regressed +130ms until this.#hash/#eql?that mutates the hash mid-probe (upstreamhash.rbtests this) now retries once instead of surfacing .NET Dictionary's concurrent-modification exception;Rehashclamps its trim similarly. (mruby raises "hash modified" here; result is unspecified but must not crash.)NoInliningslow tails; identity mode is a flag on the same comparer class so Dictionary probe sites stay monomorphic for GDV.Numbers (order-alternated pairs)
Hash#[]microSend(:eql?)per probe)Hash#[]microThe 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