Thursday, 13 October 2016

Ruby Memoization using Singleton Method




Developers often neglect the core concepts of ruby when they start working on Rails projects.

" A good rails developer may not be a good ruby developer but a good ruby developer is always a good rails developer."

In my journey to be a good ruby developer, I am demonstrating a simple but powerful core ruby concept that can be used in rails or other ruby based projects.

You might have often seen this kind of code pattern in ruby on rails projects:

  class User
      def format_name
          do_something_with name
          do_another_thing_with name
      end

      def name
          @name ||= begin
              User.first.name
          end
      end
  end

In above code name method is called multiple times and that's why we are using memoization.

Following is another way of memoization that is using some core concepts of ruby:

  class User
      def format_name
          do_something_with name
          do_another_thing_with name
      end

      def name
          def self.name
              @name
          end
          @name = User.first.name
       end
  end

Following are the concepts behind the above code:
1. Whenever you are going to call  name method  for first time, It is going to create  Singleton method - name  for declared object.
2. Whenever you will call  Singleton method  for 2nd time, it is going to execute  Singleton - name method instead of main class  name method because  Singleton method  have high preference over main class methods.
3.  Singleton method  have access to all instance variables of an object.

Above methodology can be used when code inside  begin block is complex or large or may be in different situations to simply code.

This way also memoizes `false` and `nil` values as well: 

For example:
  class Foo < Object
      def thing1
          do_something with: expensive_computation1
          do_something_else with: expensive_computation1
      end

      def thing2
          do_something with: expensive_computation2
          do_something_else with: expensive_computation2
      end
  
  private

      def expensive_computation1
          @expensive_computation1||= Model.where(id: 1742).first
      end

      def expensive_computation2
          def self.expensive_computation2
              @expensive_computation2
          end
          @expensive_computation2= Model.where(id: 4217).first
      end
  end

 thing1  and  thing2  both are memoized but  thing2  also memoizes when the record is not found.  thing1   will go hit the db again.

Benchmark comparison of different memoization methods: 


  require "benchmark"

  class A
      def name
        @name ||= begin
                    rand
                  end
      end
   end

  class B
      def name
          return(@name) if defined?(@name)
          @name = rand
       end
  end

  class C
      def name
          def self.name
              @name
          end
          @name = rand
      end
   end

  class D
      def name
          class << self
             def name
                 @name
             end
          end
          @name = rand
      end
  end

  n = 20_000
  n1 = 2_000
  Benchmark.bm(2) do |x|
      x.report("A:") { n.times { k = A.new; n1.times { k.name } } }
      x.report("B:") { n.times { k = B.new; n1.times { k.name } } }
      x.report("C:") { n.times { k = C.new; n1.times { k.name } } }
      x.report("D:") { n.times { k = D.new; n1.times { k.name } } }
  end


Output: 


           user     system      total        real
  A:   3.810000   0.000000   3.810000 (  3.817210)
  B:   4.000000   0.010000   4.010000 (  4.007852)
  C:   2.850000   0.010000   2.860000 (  2.848843)
  D:   2.850000   0.000000   2.850000 (  2.854403)



Although there is not much difference but comparatively 'C' is fastest.
Also it's worth noting that initialisation of singleton method is slower than Boolean initialisation but calling singleton method is much faster and that make it usable.

I hope you would like this new way of memoization. Suggestions to improve the code and article are most welcome. :-)

Special Credit: My friend Brian Giaraffa , Kaushik and Philip Hallstrom :-)

3 comments:

  1. Hi Aditya,

    great post! Thanks for sharing!

    I was wondering whether you could explain the differences in the benchmark results. Why are the singleton method approaches C and D faster? Is evaluating the conditionals in A and B so much more expensive?

    ReplyDelete
    Replies
    1. Hi Nicolas,

      Thanks for appreciating the post.

      Following would be the execution order of A - B:
      1. call instance method and assign value to instance variable.
      2. Do Boolean comparison on every call of method.(Boolean comparison is slow).


      Execution order of C - D:
      1. Call instance method, initialise singleton method and assign instance variable to it.(This step would be slow as compared to initialisation of A-B).
      2. In all subsequent call of instance method it's going to call singleton method instead of instance method because singleton method have high priority over instance method. It won't be required to go through the complete object to find instance method and that make it fast.

      Delete
    2. Thanks for the explanation! So it's really the Boolean comparison that does it. Very cool trick!

      Delete

Followers