Search code examples
rubyperformanceoptimizationrefactoringruby-hash

Ruby if statement optimization refactor best practice


I am having a very common refactor situation here with me, and after going through a few blogs I still didn't get any satisfactory comment on the same; so asking a question here.

h = {
  a: 'a',
  b: 'b'
}
new_hash = {}
new_hash[:a] = h[:a].upcase if h[:a].present?

According to my friend, this code can be refactored in following way to improve performance.

a = h[:a]
new_hash[:a] = a.upcase if a.present?

At first glance it looks a little optimized. But is it something that'll make a lot of difference or its an over-optimization? And which style should be preferred?

Looking for an expert advice :)

UPDATE with Benchmark n = 1000

              user     system      total        real
hash lookup  0.000000   0.000000   0.000000 (  0.000014)
new var      0.000000   0.000000   0.000000 (  0.000005)
AND op       0.000000   0.000000   0.000000 (  0.000018)
try          0.000000   0.000000   0.000000 (  0.000046)

UPDATE with Memory Benchmark using gem benchmark-memory

Calculating -------------------------------------
         hash lookup    40.000  memsize (    40.000  retained)
                         1.000  objects (     1.000  retained)
                         1.000  strings (     1.000  retained)
             new var     0.000  memsize (     0.000  retained)
                         0.000  objects (     0.000  retained)
                         0.000  strings (     0.000  retained)
              AND op    40.000  memsize (    40.000  retained)
                         1.000  objects (     1.000  retained)
                         1.000  strings (     1.000  retained)
                 try   200.000  memsize (    40.000  retained)
                         5.000  objects (     1.000  retained)
                         1.000  strings (     1.000  retained)

Solution

  • Depending on your circumstances rails methods like present? can be dirty and definitely impact performance. If you are only concerned about a nil check and not things like empty Array or blank String then using pure ruby methods will be "much" faster (the quotes are to emphasize the fact that performance is completely inconsequential in this basic example)

    Since we are benchmarking things.

    Setup

    h = {
      a: 'a',
      b: 'b'
    }
    
    
    class Object
      def present? 
        !blank?
      end
      def blank?
        respond_to?(:empty?) ? !!empty? : !self
      end
    end
    
    def hash_lookup(h)
      new_hash = {}
      new_hash[:a] = h[:a].upcase if h[:a].present?
      new_hash
    end
    
    def new_var(h)
      new_hash = {}
      a = h[:a]
      new_hash[:a] = a.upcase if a.present?
      new_hash
    end
    
    def hash_lookup_w_safe_nav(h)
      new_hash = {}
      new_hash[:a] = h[:a]&.upcase
      new_hash
    end
    
    def hash_lookup_wo_rails(h)
      new_hash = {}
      new_hash[:a] = h[:a].upcase if h[:a]
      new_hash
    end
    
    def new_var_wo_rails(h)
      new_hash = {}
      a = h[:a]
      new_hash[:a] = a.upcase if a
      new_hash
    end
    

    Benchmarks

    N = [1_000,10_000,100_000]
    require 'benchmark'
    N.each do |n|
      puts "OVER #{n} ITERATIONS"
      Benchmark.bm do |x|
        x.report(:new_var) { n.times {new_var(h)}}
        x.report(:hash_lookup) { n.times {hash_lookup(h)}}
        x.report(:hash_lookup_w_safe_nav) { n.times {hash_lookup_w_safe_nav(h)}}
        x.report(:hash_lookup_wo_rails) { n.times {hash_lookup_wo_rails(h)}}
        x.report(:new_var_wo_rails) { n.times {new_var_wo_rails(h)}}
      end
    end
    

    Output

    OVER 1000 ITERATIONS
                            user     system      total        real
    new_var                 0.001075   0.000159   0.001234 (  0.001231)
    hash_lookup             0.002441   0.000000   0.002441 (  0.002505)
    hash_lookup_w_safe_nav  0.001077   0.000000   0.001077 (  0.001077)
    hash_lookup_wo_rails    0.001100   0.000000   0.001100 (  0.001145)
    new_var_wo_rails        0.001015   0.000000   0.001015 (  0.001016)
    OVER 10000 ITERATIONS
                            user     system      total        real
    new_var                 0.010321   0.000000   0.010321 (  0.010329)
    hash_lookup             0.010104   0.000015   0.010119 (  0.010123)
    hash_lookup_w_safe_nav  0.007211   0.000000   0.007211 (  0.007213)
    hash_lookup_wo_rails    0.007508   0.000000   0.007508 (  0.017302)
    new_var_wo_rails        0.008186   0.000026   0.008212 (  0.016679)
    OVER 100000 ITERATIONS
                            user     system      total        real
    new_var                 0.099400   0.000249   0.099649 (  0.192481)
    hash_lookup             0.101419   0.000009   0.101428 (  0.199788)
    hash_lookup_w_safe_nav  0.078156   0.000010   0.078166 (  0.140796)
    hash_lookup_wo_rails    0.078743   0.000000   0.078743 (  0.166815)
    new_var_wo_rails        0.073271   0.000000   0.073271 (  0.125869)