1

I am wondering what makes if faster than short-circuit evaluation in my code:

GC.disable
N = 5000

def x
    j = nil
    N.times {
        if j
            j << 'a'.freeze
        else
            j = ''
        end
    }
    j
end

def y
    j = nil
    N.times {
        j && j << 'a'.freeze || j = ''
    }
    j
end

t = Time.now
N.times { x }
p Time.now - t

sleep 0.1

t = Time.now
N.times { y }
p Time.now - t

No matter how many times I run the code under no extra CPU load, I get:

3.826009846
4.137173916

Or something close to that.

Same goes with integers, if slightly modify the code and make j = nil, and then 0, and then add + 1 to j.

What makes the method y slower than method x?

15 Volts
  • 1,946
  • 15
  • 37
  • What version of Ruby are you on? What kind of system? I ran this on a 5 year old MBP with Ruby 2.6.4. I ran it twice with 5000 and got `x: 0.575801, y: 0.232743 ` and `x: 0.571195, y: 0.243702` – lacostenycoder Jan 11 '21 at 04:43
  • Oh wow, I have tried Ruby 2.5 and 2.7 both on GNU/Linux. 2.5 on codeanywhere PaaS and 2.7 on my own system. x was always faster by 0.2+ to 0.3+ seconds on multiple runs... – 15 Volts Jan 11 '21 at 05:46
  • I may have been mistaken about my initial comment. See my answer. – lacostenycoder Jan 11 '21 at 05:55

1 Answers1

0

UPDATED as per comment. Here's a way to improve the x method. B is your method, C is the improved version because with this syntax the right side of && never gets executed. Benchmark is the standard Ruby way of doing exactly what the class is intended to be used for easy to read docs. To see why Time.now is not recommended, see this answer. Also it's common to move the iteration to the Benchmark as to not clutter up your actual methods.

require 'benchmark'
GC.disable

N=50000

class Foo
  def a
    j = nil
    if j
        j << 'a'.freeze
    else
        j = 'not nil'
    end
    j
  end

  def b
    j = nil
    j && j << 'a'.freeze || j = 'not nil'
    j
  end

  def c
    j = nil
    j && (j << 'a' || j = 'j')
    j
  end
end
foo = Foo.new
foo.a
foo.b
foo.c
Benchmark.bmbm(10) do |x|
  x.report(:a) { N.times {foo.a }}
  puts "j is no longer nil, but it's a string #{foo.a} <- see"
  x.report(:b) { N.times {foo.b }}
  puts "j is equal to #{foo.b} which is a string, not nil"
  x.report(:c) { N.times {foo.c }}
  puts "look for j, nothing here *#{foo.c}* but emptiness"
end

puts "\n take 2\n"

Benchmark.bmbm(10) do |x|
  x.report(:c) { N.times {foo.c }}
  x.report(:a) { N.times {foo.a }}
  x.report(:b) { N.times {foo.b }}
end

Results:

j is no longer nil, but it's a string not nil <- see
j is equal to not nil which is a string, not nil
look for j, nothing here ** but emptiness
Rehearsal ----------------------------------------------
a            0.005619   0.001018   0.006637 (  0.007648)
b            0.005097   0.000924   0.006021 (  0.006847)
c            0.003488   0.000025   0.003513 (  0.003783)
------------------------------------- total: 0.016171sec

                 user     system      total        real
a            0.006934   0.000331   0.007265 (  0.010790)
b            0.004609   0.000086   0.004695 (  0.005038)
c            0.004939   0.000148   0.005087 (  0.009489)

 take 2
Rehearsal ----------------------------------------------
c            0.004118   0.000097   0.004215 (  0.005652)
a            0.005844   0.000155   0.005999 (  0.017280)
b            0.005498   0.000161   0.005659 (  0.013625)
------------------------------------- total: 0.015873sec

                 user     system      total        real
c            0.006040   0.000133   0.006173 (  0.013035)
a            0.005715   0.000081   0.005796 (  0.006672)
b            0.004042   0.000016   0.004058 (  0.004220)
lacostenycoder
  • 10,623
  • 4
  • 31
  • 48
  • 1
    I think that's 'benchmark' with downcased b. Calling the `nil?` is a bit more overhead as well. Also, I used 2 loops in my test. The reason was j is assigned to new string 5000 times, then the `||` and `if` condition is in action 5000 times. And same goes in the method, the method utilizes `&&` once j is set. Also, here `j rescue nil` is never used. j is string always, and it's doing no operation, so there's no way j raises error... Also, I didn't use benchmark library because I don't actually know how it works. Time.now - t here shows the monotonic time + time required to call `Time.now`. – 15 Volts Jan 11 '21 at 06:22
  • require works with `benchmark` or `Benchmark`. For rest see updated answer. – lacostenycoder Jan 11 '21 at 17:19
  • `LoadError (cannot load such file -- Benchmark)` – 15 Volts Jan 11 '21 at 18:32
  • 1
    try `require 'benchmark'` I updated my answer. Not sure why, but I tried with `Benchmark` on a remote linux server and got the error, but it works on my local Mac with `Benchmark`. – lacostenycoder Jan 12 '21 at 04:45