From: "byroot (Jean Boussier) via ruby-core" Date: 2026-10-31T08:45:03+00:00 Subject: [ruby-core:127286] [Ruby Bug#22488] Defining a method in a module that many classes include is ~4x slower in Ruby 4.0 than in 3.4 Issue #22488 has been updated by byroot (Jean Boussier). 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 to decide. Hence I'm marking it as requested so it is considered. ---------------------------------------- Bug #22488: Defining a method in a module that many classes include is ~4x slower in Ruby 4.0 than in 3.4 https://bugs.ruby-lang.org/issues/22488#change-119711 * Author: linyiru (Lawrence Lin) * Status: Closed * ruby -v: 4.0.9 (2026-10-23 revision 3e93b23afc) +PRISM [x86_64-linux] * Backport: 3.3: DONTNEED, 3.4: DONTNEED, 4.0: REQUIRED ---------------------------------------- ## 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 ```ruby 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? -- 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/