Symbol#to_proc is slow... is it slow enough to matter?
It's common knowledge that using the to_proc hack is slower than not. Just how much slower is it? I decided to put together a few benchmarks to find out.
Environment
These tests were run on Ruby 1.8.6-pl111 and Rails 2.1.
Benchmarking
Say there's a database of 1,000 items that for some reason you want to iterate over. Let's forget that if you're showing 1,000 items you probably have usability issues and just roll with it.
1_000.times { |n| Bar.create :name => "bar-#{n}" }
bars = Bar.find(:all)
Here's how much slower it is over a dataset of 1,000 ActiveRecord instances.
Benchmark.measure { bars.map(&:name) }.real
#=> 0.00645709037780762
Benchmark.measure { bars.map { |b| b.name } }.real
#=> 0.00141692161560059
Now that's a horrific increase: it takes more than 350% longer to run the to_proc hack than the plain block... but let's be realistic here, over 1,000 records it's taken 0.0065 seconds. Big woop. Who cares?
How about over 1,000,000 rows? We already have 1,000 rows, let's top that up.
(1_000_000 - 1_000).times { Bar.create :name => Time.now.to_f.to_s }
bars = Bar.find(:all)
That makes it 1,000,000 rows in the table. By this stage your database is probably thinking you hate it. I'm pretty confident that presenting 1,000,000 rows to the person using your application is a bit of an edge case, but hey, here's how long it takes.
Benchmark.measure { bars.map(&:name) }.real
#=> 6.25304508209229
Benchmark.measure { bars.map { |b| b.name } }.real
#=> 1.38965106010437
Almost 5 seconds extra over a million rows. Okay so 5 seconds is a pretty big hit, but how long will your application be running before you hit a million rows in one of your tables and you need to iterate over all million rows?
Don't optimise your code prematurely. By the time to_proc becomes an issue you'll already have hit many, many other problems.
Benchmark.measure { Bar.find(:all) }.real
#=> 406.738657951355
Worry about those first.
Leave feedback...
Commenting is closed for this article.

I avoid to_proc, but for readability reasons instead. The first time I saw foo(&:bar) I thought “huh, what’s this? is &:bar supposed to be a method reference to the ‘bar’ method in the ‘self’ object?”
It took a while before I realized that it’s actually passing :bar to the method as its block argument.
It’s readable once you know the trick, but quite confusing if you don’t.
I don’t know, I think this is just too easy to pass up and not optimize for speed in the first place. I consider it a proper-ruby-style thing. And I agree with Hongli, standard block syntax is more readable anyway.
Hongli Lai
Surely many of Ruby and Rails idiums are potentionally confusing unless you read up on them. The classic one is
class << self; self; end;
Until you understand Ruby singleton classes its unlikely you understand what this code is doing.
I saw an interesting suggestion regarding a memoized version of Symbol#to_proc
namely,
while rails does;
def to_proc Proc.new { |*args| args.shift.send(self, *args) }
end
instead you could do
def to_proc @to_proc ||= Proc.new { |*args| args.shift.send(self, *args) }
end
while this creates a load of Proc objects, that will never get garbage collected,
maybe its not a bad idea.
And it was quite a bit faster.
As you say, it’s silly to pre-optimize. Interestingly though I ran into a similar bottleneck in my Rails project yesterday. I am displaying a table of ~1000 objects to admins. It was taking around 10 seconds to render at this point and becoming unusable. I went through and simplified everything I could, pulled out unessential helpers (link_to, etc)… 9 seconds.
One of the key things we had was an erb helper named admin_block for showing admin-only content. Replacing that with an equivalent if statement dropped it to under one second.
If you think about what’s actually going on dynamically these bottlenecks are obvious, but it’s a tough pill to swallow that beautiful and expressive code might be horribly inefficient.
I’m curious if you’ve tried this in Ruby 1.8.7 or 1.9 where this functionality is built-in.
If anyone’s concerned about the performance loss, check out this hack.