Skip to content

Cache ObjectPType member dispatch - #187

Merged
chklauser merged 5 commits into
mainfrom
agent/cache-object-member-dispatch
Jul 25, 2026
Merged

Cache ObjectPType member dispatch#187
chklauser merged 5 commits into
mainfrom
agent/cache-object-member-dispatch

Conversation

@chklauser

Copy link
Copy Markdown
Owner

Summary

  • add bounded, DLR-style ObjectPType member call sites to interpreted functions
  • emit the same call-site access from CIL-compiled functions, while preserving Prexonite's PType identity and recent-conversion-lock semantics
  • retain four polymorphic rules, then switch permanently to a bounded megamorphic target that rebinds uncached misses
  • add BenchmarkDotNet coverage for stable hits, expensive guards, fresh receivers, retained megamorphic hits, and deoptimizing five-shape cycles, including allocation and GC statistics
  • make the CSharpier post-tool hook resolve JJ workspace roots correctly and update the README's reflection joke for the new reality

Why

ObjectPType previously repeated reflection discovery and Prexonite overload resolution for every member call. The call sites cache the result behind guards derived from the full PValue shape, rather than delegating binding to C#, so object overrides, type tags, and explicit conversions continue to follow Prexonite rules.

The four-rule cutoff bounds polymorphic-chain cost and retained memory. On the fifth distinct binding, a site keeps the four most recently bound shapes and becomes sticky-megamorphic; retained shapes still use cached bindings, while other shapes resolve directly without growing the DLR rule chain.

Benchmarks

BenchmarkDotNet 0.15.8, .NET 10, three launches, 15+ iterations, 2% maximum relative error, concurrent workstation GC:

CIL scenario uncached cached cached allocation
zero arguments 346.95 ns 74.88 ns (4.63x faster) 344 B vs 1,232 B
explicit conversion lock 595.39 ns 189.99 ns (3.13x faster) 736 B vs 1,808 B
wide guarded call 856.13 ns 436.02 ns (1.96x faster) 1,808 B vs 3,152 B
megamorphic retained hit 427.04 ns 128.60 ns (3.32x faster) 448 B vs 1,376 B
megamorphic five-shape cycle 419.30 ns 218.32 ns (1.92x faster) 707 B vs 1,376 B
genuinely fresh receiver 453.59 ns 540.23 ns (1.19x slower) 1,952 B vs 1,640 B

The fresh-receiver case is the expected unfavorable case: it cannot reuse a binding and additionally checks the bounded retained set. Saturation reduces its earlier pre-cutoff regression from roughly 1.45x to 1.19x while preventing an unbounded guard chain.

Validation

  • dotnet test -m:1: 1,446 passed, 3 skipped, 0 failed
  • dotnet build -c Release Prexonite.Benchmarks/Prexonite.Benchmarks.csproj -m:1
  • bash "$(jj root)/.codex/hooks/format-csharp.sh"
  • full 16-case CIL BenchmarkDotNet suite with MemoryDiagnoser

@chklauser
chklauser marked this pull request as ready for review July 25, 2026 00:39
@chklauser
chklauser force-pushed the agent/cache-object-member-dispatch branch from b9c49e4 to f7ed685 Compare July 25, 2026 00:42
@chklauser
chklauser merged commit 0120331 into main Jul 25, 2026
1 check passed
@chklauser
chklauser deleted the agent/cache-object-member-dispatch branch July 25, 2026 08:03
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