Relative Performance of Symbol#to_proc in Popular Ruby Implementations states that in MRI Ruby 1.8.7, Symbol#to_proc
is slower than the alternative in their benchmark by 30% to 130%, but that this isn't the case in YARV Ruby 1.9.2.
Why is this the case? The creators of 1.8.7 didn't write Symbol#to_proc
in pure Ruby.
Also, are there any gems that provide faster Symbol#to_proc performance for 1.8?
(Symbol#to_proc is starting to appear when I use ruby-prof, so I don't think I'm guilty of premature optimization)
The following ordinary Ruby code:
Will make Ruby MRI 1.8.7
Symbol#to_proc
slightly less slow than before, but not as fast as an ordinary block or a pre-existing proc.However, it'll make YARV, Rubinius and JRuby slower, hence the
if
around the monkeypatch.The slowness of using Symbol#to_proc isn't solely due to MRI 1.8.7 creating a proc each time - even if you re-use an existing one, it's still slower than using a block.
For the full benchmark and code, see https://gist.github.com/1053502
The
to_proc
implementation in 1.8.7 looks like this (seeobject.c
):Whereas the 1.9.2 implementation (see
string.c
) looks like this:If you strip away all the busy work of initializing
sym_proc_cache
, then you're left with (more or less) this:So the real difference is the 1.9.2's
to_proc
caches the generated Procs while 1.8.7 generates a brand new one every single time you callto_proc
. The performance difference between these two will be magnified by any benchmarking you do unless each iteration is done in a separate process; however, one iteration per-process would mask what you're trying to benchmark with the start-up cost.The guts of
rb_proc_new
look pretty much the same (seeeval.c
for 1.8.7 orproc.c
for 1.9.2) but 1.9.2 might benefit slightly from any performance improvements inrb_iterate
. The caching is probably the big performance difference.It is worth noting that the symbol-to-hash cache is a fixed size (67 entries but I'm not sure where 67 comes from, probably related to the number of operators and such that are commonly used for symbol-to-proc conversions):
If you use more than 67 symbols as procs or if your symbol IDs overlap (mod 67) then you won't get the full benefit of the caching.
The Rails and 1.9 programming style involves a lot of shorthands like:
rather than the longer explicit block forms:
Given that (popular) programming style, it makes sense to trade memory for speed by caching the lookup.
You're not likely to get a faster implementation from a gem as the gem would have to replace a chunk of the core Ruby functionality. You could patch the 1.9.2 caching into your 1.8.7 source though.
In addition to not caching
proc
s, 1.8.7 also creates (approximately) one array each time aproc
is called. I suspect it's because the generatedproc
creates an array to accept the arguments - this happens even with an emptyproc
that takes no arguments.Here's a script to demonstrate the 1.8.7 behavior. Only the
:diff
value is significant here, which shows the increase in array count.Sample output:
It seems that merely calling a
proc
will create the array for every iteration, but a literal block only seems to create an array once.But multi-arg blocks may still suffer from the problem:
Sample output. Note how we incur a double penalty in case #2, because we use a multi-arg block, and also call the
proc
.