Is the &method(:method_name) idiom bad for performance in Ruby?
Asked Answered
G

5

15

I've recently come across the &method(:method_name) syntax. (This uses the Object#method method - RDoc link) For example,

[5, 7, 8, 1].each(&method(:puts))

is the equivalent of

[5, 7, 8, 1].each{|number| puts number}

Are there performance penalties for the latter compared to the former in the various implementations of Ruby? If so, are the implementors working on improving its performance?

Gog answered 7/8, 2011 at 23:53 Comment(0)
C
19

Yes, it appears to be bad for performance.

def time
  start = Time.now
  yield
  "%.6f" % (Time.now - start)
end

def do_nothing(arg)
end


RUBY_VERSION # => "1.9.2"

# small
ary = *1..10
time { ary.each(&method(:do_nothing)) }     # => "0.000019"
time { ary.each { |arg| do_nothing arg } }  # => "0.000003"


# large
ary = *1..10_000
time { ary.each(&method(:do_nothing)) }     # => "0.002787"
time { ary.each { |arg| do_nothing arg } }  # => "0.001810"


# huge
ary = *1..10_000_000
time { ary.each(&method(:do_nothing)) }     # => "37.901283"
time { ary.each { |arg| do_nothing arg } }  # => "1.754063"

It looks like this is addressed in JRuby:

$ rvm use jruby
Using /Users/joshuajcheek/.rvm/gems/jruby-1.6.3

$ xmpfilter f.rb 
def time
  start = Time.now
  yield
  "%.6f" % (Time.now - start)
end

def do_nothing(arg)
end


RUBY_VERSION # => "1.8.7"

# small
ary = *1..10
time { ary.each(&method(:do_nothing)) }     # => "0.009000"
time { ary.each { |arg| do_nothing arg } }  # => "0.001000"


# large
ary = *1..10_000
time { ary.each(&method(:do_nothing)) }     # => "0.043000"
time { ary.each { |arg| do_nothing arg } }  # => "0.055000"


# huge
ary = *1..10_000_000
time { ary.each(&method(:do_nothing)) }     # => "0.427000"
time { ary.each { |arg| do_nothing arg } }  # => "0.634000"
Chinch answered 8/8, 2011 at 6:4 Comment(3)
your &:method(:do_nothing) is not analogous to do_nothing argBoulanger
Not sure what you mean.Chinch
maybe the do_nothing method receives no parameters in the first one but receives arg as a parameter in the second oneCris
S
15

Since Rubinius is the most advanced and most aggressively optimizing Ruby implementation, I asked this question on the Rubinius mailinglist, and here's what Evan Phoenix had to say:

Your assumption that it could be the same as a block is, I'm sad to say, dead wrong. There reason you don't see Method#to_proc and such in profiling is 2 fold:

  1. Most (all?) MRI profilers do not show methods that MRI defines in C, so they'd never show up.
  2. The mechanism for activating a method that has been turned into a Proc is all in C, so the overhead is invisible on the invocation side too.

Your point about the arty differences are right on. Additionally, your thinking that a VM could easily optimize it into a block is quite wrong. Object#method is a not something that would be detected and optimized away. Additionally, even with runtime optimizations, something like escape analysis is still required since #method returns a Method object that you'd have to see inside and extract the information from. On the invocation side, the invoked method can only do something special with the block in the case of block inlining, an optimization that only Rubinius has.

So to get to your questions:

  1. Does Rubinius optimize this code? No. Could it? Yes, but it's hardly easy.
  2. In time it could, yes.
  3. In time it should, yes.

Note: the questions he refers to in the last paragraph are:

  1. Does Rubinius currently optimize such point-free code?
  2. If it doesn't, could it?
  3. If it could, should it?
Slagle answered 24/8, 2011 at 1:24 Comment(3)
I've seen Symbol#to_proc show up in ruby-prof profiling of MRI 1.8 (the implementation without a name?), so I don't see why Method#to_proc wouldn't show up. However, that's the overhead for turning a symbol or method into some sort of proc. I suspect that the execution speed of the said proc may differ from that of a normal block: see the links from #6501530Gog
About profiling methods defined in C: I suspect that the non-Ruby version of perftools might be able to do that (ie "Profiling the Ruby VM and C extensions").Gog
Just wondering if the answers for the last two questions have changed or not?Patinous
H
5

It would appear they are both very similar/ the same on the latest ruby 1.9.2

# Using ruby 1.9.2-p290

require 'benchmark'

Benchmark.measure do
  1000.times { [5, 7, 8, 1].each(&method(:puts)) }
end

# =>   0.020000   0.020000   0.040000 (  0.066408)
# =>   0.020000   0.010000   0.030000 (  0.075474)
# =>   0.020000   0.020000   0.040000 (  0.048462)

Benchmark.measure do
  1000.times { [5, 7, 8, 1].each{|number| puts number} }
end

# =>   0.020000   0.020000   0.040000 (  0.071505)
# =>   0.020000   0.020000   0.040000 (  0.062571)
# =>   0.010000   0.020000   0.030000 (  0.040944)
Heber answered 8/8, 2011 at 1:41 Comment(0)
B
0

Here is a nice write up on it(just in time):

http://www.potstuck.com/2011/08/06/ruby-symbols-instead-of-blocks/

If you look closely at the profiling numbers in Mario's answer there is a slight penalty for the additional method calls as a result of calling Symbol#to_proc.

Just a guess, but I would say no, they probably won't be speeding it up anytime soon.

Bacardi answered 8/8, 2011 at 4:59 Comment(2)
The link is describing &:foo (which calls object.foo), not &method(:foo) (which calls foo(object)).Gog
Sorry about that. Clearly they are not the same call. Still a nice article though!Bacardi
B
0

as of ruby 1.9.3-p327

def time &block
  start = Time.now
  yield
  puts "%s : %.6f" % block.to_s, (Time.now - start))
end

RUBY_VERSION # => "1.9.3-p327"

# small
ary = *1..10
time { ary.each(&:to_i) }     # => "0.000010"
time { ary.each { |arg| arg.to_i } }  # => "0.000002"

# large
ary = *1..10_000
time { ary.each(&:to_i) }     # => "0.000494"
time { ary.each { |arg| arg.to_i } }  # => "0.000798"

# huge
ary = *1..10_000_000
time { ary.each(&:to_i) }     # => "0.504329"
time { ary.each { |arg| arg.to_i } }  # => "0.883390"
Boulanger answered 13/4, 2015 at 22:42 Comment(1)
This is a little different, since it's calling a method bound to the object.Salience

© 2022 - 2024 — McMap. All rights reserved.