Upgrade to Pro — share decks privately, control downloads, hide ads and more …

How to memoize

How to memoize

Memoization is an optimisation technique that works by caching the results of slow functions. It’s easy to implement, but a production-ready implementation is considerably trickier. And how do you test that your implementation is correct, and useful?

I’ll show you the tricky bits in implementing memoization, where I’ll touch on Ruby metaprogramming and memory management. Lastly, I’ll show how to measure and verify that memoization is meaningful.

Denis Defreyne

March 01, 2018
Tweet

More Decks by Denis Defreyne

Other Decks in Programming

Transcript

  1. What do you do when you have an expensive function,

    and you need to use its return value multiple times?
  2. a.depth = b.depth + 1 b.depth = [b1.depth, c.depth].max +

    1 … b2.depth = f.depth + 1 A B C D E F G H B1 B2
  3. a.depth = b.depth + 1 b.depth = [b1.depth, c.depth].max +

    1 … b2.depth = f.depth + 1 e.depth = f.depth + 1 A B C D E F G H B1 B2
  4. a.depth = b.depth + 1 b.depth = [b1.depth, c.depth].max +

    1 … b2.depth = f.depth + 1 e.depth = f.depth + 1 … A B C D E F G H B1 B2
  5. def fib(n) case n when 0 0 when 1 1

    else fib(n - 1) + fib(n - 2) end end
  6. def fib(n) case n when 0 0 when 1 1

    else fib(n - 1) + fib(n - 2) end end fib(0) = 0 fib(1) = 1 fib(2) = 1 fib(3) = 2 fib(4) = 3 fib(5) = 5 fib(6) = 8 fib(7) = 13 fib(8) = 21 fib(9) = 34
  7. fib(40) = fib(38) + fib(39) = fib(38) + fib(37) +

    fib(38) = fib(36) + fib(37) + fib(37) + fib(38)
  8. fib(40) = fib(38) + fib(39) = fib(38) + fib(37) +

    fib(38) = fib(36) + fib(37) + fib(37) + fib(38) = fib(36) + fib(37) + fib(37) + fib(36) + fib(37)
  9. fib(40) = fib(38) + fib(39) = fib(38) + fib(37) +

    fib(38) = fib(36) + fib(37) + fib(37) + fib(38) = fib(36) + fib(37) + fib(37) + fib(36) + fib(37) = …
  10. def fib(n) init = [0, 1] while init.size <= n

    init << init.last(2).sum end init[n] end
  11. def fib(n) @fib ||= {} @fib[n] ||= case n when

    0 0 when 1 1 else fib(n - 1) + fib(n - 2) end end
  12. def memoize(method_name) nonmemoized_method_name = '__nonmemoized_' + method_name.to_s alias_method nonmemoized_method_name, method_name

    define_method(method_name) do |*args| @cache ||= {} method_cache = (@cache[method_name] ||= {}) send(nonmemoized_method_name, *args) end end
  13. def memoize(method_name) nonmemoized_method_name = '__nonmemoized_' + method_name.to_s alias_method nonmemoized_method_name, method_name

    define_method(method_name) do |*args| @cache ||= {} method_cache = (@cache[method_name] ||= {}) method_cache[args] = send(nonmemoized_method_name, *args) end end
  14. def memoize(method_name) nonmemoized_method_name = '__nonmemoized_' + method_name.to_s alias_method nonmemoized_method_name, method_name

    define_method(method_name) do |*args| @cache ||= {} method_cache = (@cache[method_name] ||= {}) if method_cache.key?(args) method_cache[args] else method_cache[args] = send(nonmemoized_method_name, *args) end end end
  15. h = Node.new g = Node.new(h) f = Node.new(g) e

    = Node.new(f) d = Node.new(e) c = Node.new(d) b2 = Node.new(f) b1 = Node.new(b2) b = Node.new(c, b1) a = Node.new(b)
 
 p a.depth
 B C D E F G B1 B2 B C D E B1 B2
  16. h = Node.new g = Node.new(h) f = Node.new(g) e

    = Node.new(f) d = Node.new(e) c = Node.new(d) b2 = Node.new(f) b1 = Node.new(b2) b = Node.new(c, b1) a = Node.new(b)
 
 a.freeze
 p a.depth B C D E F G B1 B2 B C D E B1 B2
  17. def memoize(method_name) nonmemoized_method_name = '__nonmemoized_' + method_name.to_s alias_method nonmemoized_method_name, method_name

    define_method(method_name) do |*args| @cache ||= {} method_cache = (@cache[method_name] ||= {}) if method_cache.key?(args) method_cache[args] else method_cache[args] = send(nonmemoized_method_name, *args) end end end
  18. def memoize(method_name) nonmemoized_method_name = '__nonmemoized_' + method_name.to_s alias_method nonmemoized_method_name, method_name

    define_method(method_name) do |*args| @cache ||= {} method_cache = (@cache[method_name] ||= {}) if method_cache.key?(args) method_cache[args] else method_cache[args] = send(nonmemoized_method_name, *args) end end end
  19. def memoize(method_name) nonmemoized_method_name = '__nonmemoized_' + method_name.to_s alias_method nonmemoized_method_name, method_name

    method_cache = {} define_method(method_name) do |*args| instance_cache = (method_cache[self] ||= {}) if instance_cache.key?(args) instance_cache[args] else instance_cache[args] = send(nonmemoized_method_name, *args) end end end
  20. def memoize(method_name) nonmemoized_method_name = '__nonmemoized_' + method_name.to_s alias_method nonmemoized_method_name, method_name

    method_cache = {} define_method(method_name) do |*args| instance_cache = (method_cache[self] ||= {}) if instance_cache.key?(args) instance_cache[args] else instance_cache[args] = send(nonmemoized_method_name, *args) end end end
  21. require 'weakref'
 
 ref = WeakRef.new("hi")
 ref.upcase # a> HI


    GC.start
 ref.upcase # WeakRef45RefError (Invalid Reference - probably recycled)
  22. define_method(method_name) do |*args| instance_cache = (method_cache[self] ||= {}) value =

    instance_cache[args]&.object if value value else instance_cache[args] = Ref45SoftReference.new( send(original_method_name, *args) ) end end
  23. define_method(method_name) do |*args| instance_cache = (method_cache[self] ||= {}) value =

    instance_cache[args]&.object if wrapper wrapper.value else value = send(original_method_name, *args) wrapper = ValueWrapper.new(value) instance_cache[args] = RefnoSoftReference.new(wrapper) value end end
  24. We now have a memoization mechanism that… * … has

    a nice API * … supports multiple arguments * … supports frozen objects * … is memory-efficient * … and soon will be thread-safe
  25. require 'ddmemoize' class FibFast DDMemoize.activate(self) memoized def run(n) if n

    == 0 0 elsif n == 1 1 else run(n - 1) + run(n - 2) end end end
  26. Q&A