Skip to content

Flyweight TypeName / Namespace cached in a shared trie#2957

Open
tk0miya wants to merge 1 commit into
ruby:masterfrom
tk0miya:claude/memoize-hash-and-resolver-cache
Open

Flyweight TypeName / Namespace cached in a shared trie#2957
tk0miya wants to merge 1 commit into
ruby:masterfrom
tk0miya:claude/memoize-hash-and-resolver-cache

Conversation

@tk0miya
Copy link
Copy Markdown
Contributor

@tk0miya tk0miya commented May 16, 2026

Cache canonical TypeName / Namespace instances behind two positional factory methods, and route every allocation site — Ruby helpers, the C parser, the resolver, and the environment — through them. Repeated construction of structurally equal values returns the same object instead of allocating fresh.

Namespace[path, absolute]
TypeName[namespace, name]

Namespace interns into a per-absolute trie of nested Hashes keyed on path Symbols. TypeName interns into a two-level Hash keyed by canonical Namespace identity and name Symbol. Both fast paths are lock-free; cache misses take a mutex.

The C parser calls Namespace[] / TypeName[] via rb_funcallv; an in-C trie walk that read intern internals through rb_hash_lookup was identical in wall time on Ruby 4.0+ and was dropped to avoid coupling the parser to the cache layout.

Measured against the kaigionrails/conference-app project's RBS collection (3,903 type names), 1 warmup + 5 runs, median:

Ruby 3.4.9 master 1.247s wall / 0.63s user
branch 1.158s wall / 0.57s user (-7% wall, -10% user)
Ruby 4.0.4 master 1.175s wall / 0.57s user
branch 1.146s wall / 0.55s user (-2.5% wall, -4% user)

Live RBS::Namespace 51,436 → 6,904; RBS::TypeName 78,493 → 22,819 after rbs list on Ruby 4.0.

@tk0miya
Copy link
Copy Markdown
Contributor Author

tk0miya commented May 16, 2026

This also improves the performance of steep (with kaigionrails/conference-app):
(-4.2% wall, -5.5% user)

$ ruby -v
ruby 4.0.4 (2026-05-12 revision b89eb1bcbf) +PRISM [arm64-darwin24]
$ time bundle exec steep check > /dev/null
bundle exec steep check > /dev/null  32.32s user 1.84s system 570% cpu 5.991 total
$ time bundle exec steep check > /dev/null
bundle exec steep check > /dev/null  32.40s user 1.84s system 599% cpu 5.707 total
$ time bundle exec steep check > /dev/null
bundle exec steep check > /dev/null  32.45s user 1.81s system 597% cpu 5.730 total
$ time bundle exec steep check > /dev/null
bundle exec steep check > /dev/null  32.22s user 1.82s system 599% cpu 5.676 total
$ time bundle exec steep check > /dev/null
bundle exec steep check > /dev/null  32.37s user 1.89s system 593% cpu 5.771 total
$ ruby -v
ruby 4.0.4 (2026-05-12 revision b89eb1bcbf) +PRISM [arm64-darwin24]
$ time bundle exec steep check > /dev/null
bundle exec steep check > /dev/null  30.96s user 1.94s system 541% cpu 6.072 total
$ time bundle exec steep check > /dev/null
bundle exec steep check > /dev/null  30.74s user 1.83s system 586% cpu 5.551 total
$ time bundle exec steep check > /dev/null
bundle exec steep check > /dev/null  30.71s user 1.81s system 592% cpu 5.488 total
$ time bundle exec steep check > /dev/null
bundle exec steep check > /dev/null  30.06s user 1.81s system 593% cpu 5.371 total
$ time bundle exec steep check > /dev/null
bundle exec steep check > /dev/null  29.93s user 1.90s system 577% cpu 5.510 total
$ time bundle exec steep check > /dev/null
bundle exec steep check > /dev/null  30.48s user 1.88s system 591% cpu 5.472 total

@tk0miya tk0miya force-pushed the claude/memoize-hash-and-resolver-cache branch from 049a778 to 543012c Compare May 16, 2026 08:51
Cache canonical TypeName / Namespace instances behind two
positional factory methods, and route every allocation site —
Ruby helpers, the C parser, the resolver, and the environment
— through them. Repeated construction of structurally equal
values returns the same object instead of allocating fresh.

  Namespace[path, absolute]
  TypeName[namespace, name]

Namespace interns into a per-`absolute` trie of nested Hashes
keyed on path Symbols. TypeName interns into a two-level Hash
keyed by canonical Namespace identity and name Symbol. Both
fast paths are lock-free; cache misses take a mutex.

The C parser calls `Namespace[]` / `TypeName[]` via
`rb_funcallv`; an in-C trie walk that read intern internals
through `rb_hash_lookup` was identical in wall time on Ruby 4.0+
and was dropped to avoid coupling the parser to the cache layout.

Measured against the kaigionrails/conference-app project's RBS
collection (3,903 type names), 1 warmup + 5 runs, median:

  Ruby 3.4.9   master 1.247s wall / 0.63s user
               branch 1.158s wall / 0.57s user   (-7% wall, -10% user)
  Ruby 4.0.4   master 1.175s wall / 0.57s user
               branch 1.146s wall / 0.55s user   (-2.5% wall, -4% user)

Live RBS::Namespace 51,436 → 6,904; RBS::TypeName 78,493 → 22,819
after `rbs list` on Ruby 4.0.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@tk0miya tk0miya force-pushed the claude/memoize-hash-and-resolver-cache branch from 543012c to a35ba9f Compare May 16, 2026 10:58
Copy link
Copy Markdown
Member

@soutaro soutaro left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm. This requires a difficult architectural decision. What if we want to multi-ractor processing in RBS?

Let's merge this for now, but we may revisit this when we want to make things in RBS parallel for more performance later.

@soutaro soutaro added this to the RBS 4.1 milestone May 18, 2026
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.

2 participants