Procs, lambdas, and performance

**** EDIT – New stuff added 9/28 at the bottom to fix this and make it more accurate, thanks to reddit user estum ****

Somehow or another I ended up reading about procs and lambdas in Ruby today, and since I personally haven’t really used them, I wondered if they have any performance benefits over using a block like I normally do. So I tested it out with benchmark. This article is pretty helpful, with examples. There is also the ruby docs if you prefer that (this link goes to the doc on procs).

I spun up a ruby file and used this to test it out.

require 'benchmark'

test_proc = Proc.new { |x| x * x }
test_lambda = ->(x) { x * x }

def call_block(x)
  yield(x)
end

n = 1_000_000_000

Benchmark.bm do |x|
  x.report("block:")   { n.times { call_block(5) { |x| x * x } } }
  x.report("proc:")    { n.times { test_proc.call(5) } }
  x.report("lambda:")  { n.times { test_lambda.call(5) } }
end

A quick run through on what is going on here ^^

Make sure you require benchmark at the top

require 'benchmark'

And then create a proc and lambda

test_proc = Proc.new { |x| x * x }
test_lambda = ->(x) { x * x }

As well as a block

def call_block(x)
  yield(x)
end

and then set the number of iterations to see how fast it goes (setting it at a billion took some time, so next time maybe I’ll do a smaller number)

n = 1_000_000_000

That is going to call each of them a billion times to assess the performance

And finally, setting up something to assess the performance

Benchmark.bm do |x|
  x.report("block:")   { n.times { call_block(5) { |x| x * x } } }
  x.report("proc:")    { n.times { test_proc.call(5) } }
  x.report("lambda:")  { n.times { test_lambda.call(5) } }
end

It is going to set x as 5 to do 5 * 5 3 billion times total

And then I run it

ruby proc_lambda.rb

And here are the results

       user     system      total        real
block: 41.166534   0.101327  41.267861 ( 41.412893)
proc: 36.837533   0.078706  36.916239 ( 36.960835)
lambda: 36.432841   0.077678  36.510519 ( 36.594011)

So everything I read said that block should have been quite a bit faster. But the way I have it is increasing the overhead when you use the method call_block that has a yield inside of it, instead of just going straight to the block. The proc and lambda only needed to get created once, and the block was passing a method every single time. So here is a solution to not calling the method every time and just passing it directly.

require 'benchmark'

test_proc = Proc.new { |x| x * x }
test_lambda = ->(x) { x * x }

n = 1_000_000_000

Benchmark.bm do |x|
  # No method, just a block
  x.report("block:")   { n.times { |i| i * i } }
  
  # same as earlier
  x.report("proc:")    { n.times { test_proc.call(5) } }
  
  # same as earlier
  x.report("lambda:")  { n.times { test_lambda.call(5) } }
end

So I probably should have done it like that the first time, but now I know. Here is the benchmark for that one.

       user     system      total        real
block: 19.368932   0.041246  19.410178 ( 19.410762)
proc: 36.174256   0.100088  36.274344 ( 36.415370)
lambda: 35.885171   0.070823  35.955994 ( 35.957910)


And it was pointed out that what I did wasn’t exactly accurate. Part of the reason I post this stuff is because I still have a ton to learn, so that works out great.

I needed to update the way that proc and lambda were being passed into the loop and avoid unnecessary overhead. I think .call was getting used way too often, and needed to be replaced with & to pass them directly to n.times. Like this

Benchmark.bm do |x|
  x.report("block:")   { n.times { |i| i * i } }
  x.report("proc:")    { n.times(&test_proc) }
  x.report("lambda:")  { n.times(&test_lambda) }
end

So not as drastic as the split above, but a little faster still.

       user     system      total        real
block: 33.301507   0.070785  33.372292 ( 33.379451)
proc: 35.709916   0.101752  35.811668 ( 35.992860)
lambda: 35.555532   0.107621  35.663153 ( 35.837953)

I think this is more accurate now, but if I’m wrong, please let me know


Comments

One response to “Procs, lambdas, and performance”

Leave a Reply

Discover more from Brendan Bondurant

Subscribe now to keep reading and get access to the full archive.

Continue reading