Hi guys,
I recently discussed the `delegate` method with @tenderlove in
connection with GitHub issue #2711. I wished to replace
a series of "manual" delegations (using method declarations) with a
single call to `delegate`. Mr. @tenderlove correctly
pointed out that this would incur a deterioration of performance.
The current implementation of `delegate` is basically this, sans all
the bells and whistles:
def delegate(target, method)
class_eval(<<-RUBY)
def #{method}(*args, &block)
#{target}.__send__(:#{method}, *args, &block)
end
RUBY
end
He also identified four run-time issues which hurt performance
(paraphrased):
1. Stack depth impacts GC time
2. Paying an extra method call `__send__`
3. `*args` contraction (we must build an array that is just GC'd)
4. Splatting the args back to the `__send__`
While I cannot currently find a way to improve 3 and 4, I believe we
can alleviate the problems caused by the increased
stack depth and the overhead of the extra method call. These problems
both arise due to the use of `__send__`. Eliminating
the call yields:
def delegate(target, method)
class_eval(<<-RUBY)
def #{method}(*args, &block)
#{target}.#{method}(*args, &block)
end
RUBY
end
I ran a benchmark of the different implementations (https://
gist.github.com/1178156) using MRI versions 1.8.7 and 1.9.2.
The "method" row is the baseline, i.e. a manual delegation using a
full method definition. I'd love to see more people run
the benchmark to see how the results stack up.
Using MRI 1.8.7:
user system total real
method 8.460000 0.010000 8.470000 ( 8.477583)
old 12.960000 0.010000 12.970000 ( 12.978188)
new 9.350000 0.010000 9.360000 ( 9.365799)
Using MRI 1.9.2:
user system total real
method 4.340000 0.000000 4.340000 ( 4.349549)
old 4.670000 0.000000 4.670000 ( 4.664780)
new 4.480000 0.000000 4.480000 ( 4.477026)
The new implementation is clearly faster than the old, especially on
1.8.7.
There are, however, two caveats to this new implementation:
1. Writer methods (those ending in "=") fail spectacularly. This is
due to the fact that you cannot directly call such a
method with more than one argument, i.e. `foo.bar=(1, 2)` is not
syntactically valid, while `foo.__send__(:bar, 1, 2)`
is. This can be solved by defining the argument list differently
for such methods - which would also benefit performance!
2. The semantics of `delegate` are slightly changed: it is no longer
possible to delegate to private methods.
The second issue is something that should be discussed here. I'm
biased towards loose coupling, and therefore believe
calling private methods on another object is inherently evil. It
*will* be a problem with regards to backwards compatibility,
assuming people actually use `delegate` for such a nefarious purpose.
I hope people will be interested in discussing this topic, as I think
`delegate` is an important part of the "plumbing" of
Rails, and deserves all the optimization we can muster. This is
especially true when we seize to use it internally because
it is too slow.
Cheers,
Daniel Schierbeck (@dasch)
--
You received this message because you are subscribed to the Google Groups "Ruby
on Rails: Core" group.
To post to this group, send email to [email protected].
To unsubscribe from this group, send email to
[email protected].
For more options, visit this group at
http://groups.google.com/group/rubyonrails-core?hl=en.