Bug #22488
closedDefining a method in a module that many classes include is ~4x slower in Ruby 4.0 than in 3.4
Description
Summary¶
This affects the released 4.0 series (4.0.0 to 4.0.9), and is already fixed on master.
In Ruby 4.0, defining a method in a module costs more for every class that includes it than in Ruby 3.4: with 500 including classes, defining 2,000 methods takes 77 ms on 4.0.9 against 20 ms on 3.4.12. Master is back to 20 ms since 14eb0831527b8e1c5bbfef21ecaed14b21103315 ("Speedup repeated calls to rb_gccct_clear_table"), which isn't on ruby_4_0. It applies cleanly there and brings 4.0 back to the 3.4 numbers.
This pattern is common in Rails applications: the route helper modules are included in every controller, view and mailer by the time the routes are drawn, and each named route defines two methods in them.
Reproduction¶
def measure(includers, methods: 2000)
rd, wr = IO.pipe
pid = fork do
rd.close
mod = Module.new
base = Class.new { def warm; end }
classes = includers.times.map do
c = Class.new(base) { include mod }
c.new.warm
c
end
t = Process.clock_gettime(Process::CLOCK_MONOTONIC)
methods.times { |i| mod.define_method(:"m#{i}") { i } }
wr.write(((Process.clock_gettime(Process::CLOCK_MONOTONIC) - t) * 1000).to_s)
exit!(0)
end
wr.close
Process.wait(pid)
rd.read.to_f
end
[0, 100, 500].each do |n|
puts format("%3d including classes: %.1f ms", n, 3.times.map { measure(n) }.min)
end
The classes need to stay referenced: if they are collected during the loop, the slowdown disappears.
Results¶
The script above on x86_64 Linux (Ryzen 7 8745HS), with the official ruby Docker images:
| 0 including classes | 100 | 500 | |
|---|---|---|---|
| 3.4.12 | 5.0 ms | 7.3 ms | 19.5 ms |
| 4.0.9 | 4.4 ms | 18.7 ms | 76.5 ms |
| 4.1.0-preview1 | 2.1 ms | 5.4 ms | 19.2 ms |
4.0.0 gives the same numbers as 4.0.9 (77 ms with 500 including classes). With miniruby built from ruby_4_0 (9bebcb455f), the same measurement gives 77 ms, and 20 ms with 14eb0831 cherry-picked.
Bisecting with miniruby:
- The slowdown starts at 382645d440d5da66a0c04557f3ff2ca226de3a27 ("namespace on read"): 16 ms on its parent, 78 ms on it, with 500 including classes.
- It ends at 14eb0831527b8e1c5bbfef21ecaed14b21103315 ("Speedup repeated calls to
rb_gccct_clear_table"): 78 ms on its parent, 21 ms on it.
git cherry-pick 14eb0831 applies cleanly on ruby_4_0 (5 files, 11 insertions, 7 deletions).
Impact on a Rails application¶
Booting Mastodon (Rails 8.1.4, 605 named routes, the route helpers included in 643 classes and modules) in production, on Ruby 4.0.9 built from source with the same configure flags as the official image, with and without 14eb0831, 7 alternating runs:
| 4.0.9 | 4.0.9 + 14eb0831 | |
|---|---|---|
| Boot, median | 2087 ms | 2044 ms |
| Drawing the routes, median | 205 ms | 188 ms |
The boot improves by more than the route drawing, as every method defined while booting is cheaper.
Since the fix is small and applies cleanly, could it be considered for backporting to 4.0?
Updated by byroot (Jean Boussier) 6 days ago
- Status changed from Open to Closed
- Backport changed from 3.3: UNKNOWN, 3.4: UNKNOWN, 4.0: UNKNOWN to 3.3: DONTNEED, 3.4: DONTNEED, 4.0: REQUIRED
As mentioned on GitHub, we don't normally backport performance patches, but ultimately it's the branch maintainer's call so it's for @k0kubun (Takashi Kokubun) to decide. Hence I'm marking it as requested so it is considered.