Skip to content

Commit 5c8ab6c

Browse files
committed
Optimize DelegateClass using ... delegation
By generating source code for methods that use `...` delegation when possible, we can lower the overhead of delegation by half. This could be lowered further by copying the delegated method signature, like in ruby#16, but this would assume the delegated method signature never change, so I'm not sure if that's OK. Then most of the remaining overhead is in calling `__getobj__`, but that's part of the spec, so can't be eliminated. Results: ``` == no arguments == ruby 3.4.6 (2025-09-16 revision dbd83256b1) +YJIT +PRISM [arm64-darwin24] Warming up -------------------------------------- baseline 3.838M i/100ms handrolled 3.465M i/100ms DelegateClass 979.160k i/100ms Opt 2.028M i/100ms Calculating ------------------------------------- baseline 64.296M (± 0.5%) i/s (15.55 ns/i) - 322.355M in 5.013724s handrolled 57.058M (± 0.4%) i/s (17.53 ns/i) - 287.567M in 5.039966s DelegateClass 12.118M (± 0.5%) i/s (82.52 ns/i) - 60.708M in 5.009812s Opt 27.764M (± 0.5%) i/s (36.02 ns/i) - 139.925M in 5.039997s Comparison: baseline: 64296345.8 i/s handrolled: 57058063.8 i/s - 1.13x slower Opt: 27763713.5 i/s - 2.32x slower DelegateClass: 12118085.0 i/s - 5.31x slower == many arguments == ruby 3.4.6 (2025-09-16 revision dbd83256b1) +YJIT +PRISM [arm64-darwin24] Warming up -------------------------------------- baseline 3.605M i/100ms handrolled 3.275M i/100ms DelegateClass 623.030k i/100ms Opt 1.348M i/100ms Calculating ------------------------------------- baseline 63.349M (± 1.6%) i/s (15.79 ns/i) - 317.272M in 5.009667s handrolled 56.277M (± 0.2%) i/s (17.77 ns/i) - 281.623M in 5.004270s DelegateClass 7.079M (± 4.1%) i/s (141.26 ns/i) - 35.513M in 5.026286s Opt 17.953M (± 0.1%) i/s (55.70 ns/i) - 90.345M in 5.032248s Comparison: baseline: 63348844.0 i/s handrolled: 56276912.5 i/s - 1.13x slower Opt: 17953308.5 i/s - 3.53x slower DelegateClass: 7079118.4 i/s - 8.95x slower ``` Benchmark: ```ruby require "delegate" require "bundler/inline" gemfile do gem "benchmark-ips" end class User attr_accessor :name def initialize(name) @name = name end def do_something(a, b, c, d: 1) :something end end class HandrolledDelegator def initialize(user) @user = user end def name @user.name end def do_something(a, b, c, d: 1) @user.do_something(a, b, c, d: 1) end end StdlibDelegator = DelegateClass(User) def OptDelegateClass(superclass, &block) klass = Class.new(Delegator) ignores = [*::Delegator.public_api, :to_s, :inspect, :=~, :!~, :===] protected_instance_methods = superclass.protected_instance_methods protected_instance_methods -= ignores public_instance_methods = superclass.public_instance_methods public_instance_methods -= ignores instance_methods = (public_instance_methods + protected_instance_methods) normal, special = instance_methods.partition { |m| m.match?(/\A[a-zA-Z]\w*\z/) } source = normal.map do |method| "def #{method}(...); __getobj__.#{method}(...); end" end klass.module_eval do def __getobj__ # :nodoc: unless defined?(@delegate_dc_obj) return yield if block_given? __raise__ ::ArgumentError, "not delegated" end @delegate_dc_obj end def __setobj__(obj) # :nodoc: __raise__ ::ArgumentError, "cannot delegate to self" if self.equal?(obj) @delegate_dc_obj = obj end class_eval(source.join(";"), __FILE__, __LINE__) special.each do |method| define_method(method, Delegator.delegating_block(method)) end protected(*protected_instance_methods) end klass.define_singleton_method :public_instance_methods do |all=true| super(all) | superclass.public_instance_methods end klass.define_singleton_method :protected_instance_methods do |all=true| super(all) | superclass.protected_instance_methods end klass.define_singleton_method :instance_methods do |all=true| super(all) | superclass.instance_methods end klass.define_singleton_method :public_instance_method do |name| super(name) rescue NameError raise unless self.public_instance_methods.include?(name) superclass.public_instance_method(name) end klass.define_singleton_method :instance_method do |name| super(name) rescue NameError raise unless self.instance_methods.include?(name) superclass.instance_method(name) end klass.module_eval(&block) if block return klass end OptStdlibDelegator = OptDelegateClass(User) direct = User.new("George") handrolled = HandrolledDelegator.new(direct) stdlib = StdlibDelegator.new(direct) opt_stdlib = OptStdlibDelegator.new(direct) puts "== no arguments ==" Benchmark.ips do |x| x.report("baseline") { direct.name } x.report("handrolled") { handrolled.name } x.report("DelegateClass") { stdlib.name } x.report("Opt") { opt_stdlib.name } x.compare!(order: :baseline) end puts "== many arguments ==" Benchmark.ips do |x| x.report("baseline") { direct.do_something(1, 2, 3, d: 4) } x.report("handrolled") { handrolled.do_something(1, 2, 3, d: 4) } x.report("DelegateClass") { stdlib.do_something(1, 2, 3, d: 4) } x.report("Opt") { opt_stdlib.do_something(1, 2, 3, d: 4) } x.compare!(order: :baseline) end ```
1 parent c8beb7f commit 5c8ab6c

File tree

1 file changed

+16
-5
lines changed

1 file changed

+16
-5
lines changed

lib/delegate.rb

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -399,6 +399,14 @@ def DelegateClass(superclass, &block)
399399
protected_instance_methods -= ignores
400400
public_instance_methods = superclass.public_instance_methods
401401
public_instance_methods -= ignores
402+
403+
instance_methods = (public_instance_methods + protected_instance_methods)
404+
normal, special = instance_methods.partition { |m| m.match?(/\A[a-zA-Z]\w*!?\z/) }
405+
406+
source = normal.map do |method|
407+
"def #{method}(...); __getobj__.#{method}(...); end"
408+
end
409+
402410
klass.module_eval do
403411
def __getobj__ # :nodoc:
404412
unless defined?(@delegate_dc_obj)
@@ -407,18 +415,21 @@ def __getobj__ # :nodoc:
407415
end
408416
@delegate_dc_obj
409417
end
418+
410419
def __setobj__(obj) # :nodoc:
411420
__raise__ ::ArgumentError, "cannot delegate to self" if self.equal?(obj)
412421
@delegate_dc_obj = obj
413422
end
414-
protected_instance_methods.each do |method|
415-
define_method(method, Delegator.delegating_block(method))
416-
protected method
417-
end
418-
public_instance_methods.each do |method|
423+
424+
class_eval(source.join(";"), __FILE__, __LINE__)
425+
426+
special.each do |method|
419427
define_method(method, Delegator.delegating_block(method))
420428
end
429+
430+
protected(*protected_instance_methods)
421431
end
432+
422433
klass.define_singleton_method :public_instance_methods do |all=true|
423434
super(all) | superclass.public_instance_methods
424435
end

0 commit comments

Comments
 (0)