[ruby-core:121457] [Ruby Bug#21201] Performance regression when defining methods inside `refine` blocks
From:
"jhawthorn (John Hawthorn) via ruby-core" <ruby-core@...>
Date:
2025-03-27 18:11:26 UTC
List:
ruby-core #121457
Issue #21201 has been updated by jhawthorn (John Hawthorn).
tenderlovemaking (Aaron Patterson) wrote in #note-2:
> I guess if refined methods are rare enough, maybe it's not worthwhile to allocate "refinement inline cache"? In other words, refined methods are always an IC miss?
I believe that was the case before Ruby 3.3
----------------------------------------
Bug #21201: Performance regression when defining methods inside `refine` blocks
https://bugs.ruby-lang.org/issues/21201#change-112457
* Author: alpaca-tc (Hiroyuki Ishii)
* Status: Open
* ruby -v: 3.3.7, 3.4.2, 3.5.0
* Backport: 3.1: UNKNOWN, 3.2: UNKNOWN, 3.3: UNKNOWN, 3.4: UNKNOWN
----------------------------------------
After the change introduced in [PR #10037](https://github.com/ruby/ruby/pull/10037), a significant performance regression has been observed when defining methods within refine blocks.
The PR correctly fixes a bug where `callcache` invalidation was missing when methods are defined inside a refinement. To fix this, it now invokes [`rb_clear_all_refinement_method_cache()`](https://github.com/ruby/ruby/blob/752a1d785475d1950b33c7d77b289a6a3a753f02/vm_method.c#L439-L443) at the time of method definition within `refine`. However, this function performs a full object space traversal via `rb_objspace_each_objects`, making it extremely expensive.
In our team's Rails application, refinements are used heavily. During application code reload (triggered in development mode), `rb_clear_all_refinement_method_cache()` is called 1425 times, 1142 of which originate via `rb_clear_method_cache()`. As a result, the code reload time has increased from **5 seconds** to **15 seconds** after the patch. Since reloading occurs every time the code changes, this degrades development experience severely.
### Reproduction script:
```ruby
require "bundler/inline"
gemfile(true) do
source "https://rubygems.org"
gem "benchmark-ips"
end
mod = Module.new
klass = Class.new
Benchmark.ips do |x|
x.report(RUBY_VERSION) do
mod.send(:refine, klass) do
def call_1 = nil
def call_2 = nil
def call_3 = nil
end
end
x.save! "/tmp/performance_regression_refine.bench"
x.compare!
end
```
### Benchmark results:
```
Comparison:
3.2.7: 1500316.7 i/s
3.3.7: 158.0 i/s - 9496.46x slower
3.5.0: 124.6 i/s - 12041.43x slower
3.4.2: 119.5 i/s - 12553.50x slower
```
Related Slack discussion: https://ruby-jp.slack.com/archives/CLWSHA76V/p1741932018811539
I totally understand the necessity and value of the past PR that fixed the callcache bugs. That said, I was wondering if there might be any ideas to make it faster, or to handle the invalidation more efficiently.
--
https://bugs.ruby-lang.org/
______________________________________________
ruby-core mailing list -- ruby-core@ml.ruby-lang.org
To unsubscribe send an email to ruby-core-leave@ml.ruby-lang.org
ruby-core info -- https://ml.ruby-lang.org/mailman3/lists/ruby-core.ml.ruby-lang.org/