Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
branch: master
Fetching contributors…

Cannot retrieve contributors at this time

57 lines (39 sloc) 3.275 kb
format title published_on
textile
Optimising Symbol#to_proc
Mon Nov 26 10:10:00 UTC 2007

Whilst pairing with Paul the other day I noticed that he preferred not to use Symbol#to_proc; on asking why, he told me it was because of the unnecessary performance hit that Symbol#to_proc imposed.

Now I’m not one for premature optimisation, but with an idiom like Symbol#to_proc likely to be used throughout a codebase, performance hits like this add up and as things stand, the Rails implementation of Symbol#to_proc is pretty expensive:

require 'benchmark'
require 'rubygems'
require 'active_support'

BIG_ARRAY = ['x'] * 1000000

Benchmark.bm do |bm|
  bm.report("Standard block") do
    BIG_ARRAY.map { |c| c.upcase }
  end
  
  bm.report("Symbol#to_proc") do
    BIG_ARRAY.map(&:upcase)
  end
end

Output on my 2Ghz quad-core Mac Pro:

 	            user     system      total        real
Standard block  0.720000   0.060000   0.780000 (  0.772927)
Symbol#to_proc  3.030000   0.010000   3.040000 (  3.040889)

Ouch. That’s roughly four times slower. -Based on my naive understanding of how Symbol#to_proc was implemented, it figured that the bottleneck was the creation of a new Proc object for every iteration; the proc doesn’t need to change for each iteration so surely we could just memoize it-?

Update: It seems that my initial assumption was incorrect; to_proc is in fact only called once. The real issue here is not the instantiation of a new proc, but the Rails implementation. Rails uses this slightly more complicated implementation in order to support passing of multiple arguments with the method call:

Proc.new { |*args| args.shift.__send__(self, *args) }

This allows you to do a few neat things with multiple elements of a collection like [1, 2, 3].inject(&:+) but I consider this supporting an edge case at the expense of performance.

I’ve never found myself in need of the functionality provided by Rails’ implementation (I didn’t even know it was supported) but I do find myself using the obj.map(&:method) idiom a lot so the following simplified implementation suits me just fine:

class Symbol
  def to_proc
    proc { |obj| obj.__send__(self) }
  end
end

The performance gain is significant:

                user     system      total        real
Symbol#to_proc  0.910000   0.010000   0.920000 (  0.916718)

The implementation itself is trivial but I’ve made it available on pastie – just drop it into a file somewhere in your Rails lib folder. If I find the time, I will try and package it up as a basic Rails plugin too. It’s worth bearing in mind that Ruby 1.9’s implementation will probably support the passing of arguments like the Rails implementation but hopefully it should be much faster.

The performance of Symbol#to_proc has also been brought up on Pratik Naik’s blog and this Rails ticket.

Update: Some of my original assumptions about the way Ruby invokes to_proc were incorrect and I have updated my article accordingly.

Jump to Line
Something went wrong with that request. Please try again.