Лекция 10: Кэширование значений методов. MakeCached
Идея кэширования
# без кэширования def fib(n) (n < 2) ? 1 : fib(n-1) + fib(n-2) end # с кэшированием (memoization) def fib(n) (@fib ||= {})[n] ||= ( (n < 2) ? 1 : fib(n-1) + fib(n-2) ) end (1..1000).each do |i| p fib(i) end
Простейший вариант без метапрограммирования
С помощью оператора alias копирования метода (создание копии байткода имеющегося метода под новым именем) можно добавить кэширование к любому методу:# библиотечный файл # без кэширования def fib(n) (n < 2) ? 1 : fib(n-1) + fib(n-2) end ################################### # наш файл # создан метод fib_orig, копия метода fib # в теле метода fib_orig по-прежнему вызывается функция fib alias fib_orig fib def fib(n) (@fib ||= {})[n] ||= fib_orig(n) end
alias foo_orig foo def foo(*args) (@foo ||= {})[args] ||= foo_orig(*args) end
Простейший вариант с метапрограммированием используем eval
Добавление кэширование к методу стандартная задача. Хотелось бы иметь универсальное решение в одну строку. Например, такое:def make_cached(method) code = " alias foo_orig foo def foo(*args) (@foo ||= {})[args] ||= foo_orig(*args) end " eval code.gsub('foo', method) end nake_cached :fib make_cached :foo
Решение без eval
Можно обойтись без использования eval. Здесь поможет метод define_method.class Module # определим метод alias_method, # который лучше чем оператор alias тем, что является методом # и ему можно передавать не только имена методов, # но и переменные, содержащие имена методов def alias_method(a,b) class_eval "alias #{a} #{b}" end def make_cached(method) orig_method = "#{method}_orig" cache = {} alias_method orig_method, method define_method(method) do |*args| cache[args] ||= send(orig_method, *args) end end end class A def fib(n) (n < 2) ? 1 : fib(n-1) + fib(n-2) end make_cached :fib end p A.new.fib(1000)
Решение без использования функции eval, работающее и для глобальных методов
class Module def alias_method(a,b) class_eval "alias #{a} #{b}" end end module MakeCached def make_cached(method) orig_method = "#{method}_orig" cache = {} (self.is_a?(Module) ? self : self.singleton_class).class_eval do alias_method orig_method, method define_method(method) do |*args| cache[args] ||= send(orig_method, *args) end end end end class Object def singleton_class (class << self; self; end) end end Object.module_eval { include MakeCached } def fib(n) (n < 2) ? 1 : fib(n-1) + fib(n-2) end make_cached :fib p fib(1000) class A def fib2(n) (n < 2) ? 1 : fib2(n-1) + fib2(n-2) end make_cached :fib2 end p A.new.fib(1000)
alias_method_chain спасение от коллизий патчей
Модификация существующих методов с помощью alias_method называется monkey patch. Мы модифицируем имеющийся метод, дописывая какой-то код в начало, в конец или окружая метод каким-то кодом. Другой простейший пример monkey patch это make_traced:module MakeCached def make_traced(*methods) methods.each do |method| orig_method = "#{method}_orig" (self.is_a?(Module) ? self : self.singleton_class).class_eval do alias_method orig_method, method define_method(method) do |*args| puts "#{method}#{args.join(', ')}" send(orig_method, *args) end end end end end
make_traced :fib fib(5)
make_traced :fib make_cached:fib fib(5)
Объясните почему здесь возникает stack overflow.
Решение этой проблемы заключено в технике alias_method_chain.
Вот ее суть:
def fib(n) (n < 2) ? 1 : fib(n-1) + fib(n-2) end def fib_with_traced(n) puts "fib(#{n})" fib_without_traced(n) end alias fib_without_traced fib alias fib fib_with_traced
# вместо alias fib_without_traced fib alias fib fib_with_traced # пишут alias_method_chain :fib, :traced
Опция timeout устаревание закэшированных значений
Добавим новую фичу устаревание закэшированных значений.class Module def method_alias(a,b) eval "alias #{a} #{b}" end def alias_method_chain(method, feature) alias_method "#{method}_without_#{feature}", method alias_method method, "#{method}_with_#{feature}" end end class Object def singleton_class (class <<self; self; end) end end def extract_options(args) args.last.is_a?(Hash) ? args.pop : {} end module MethodModifiers def make_cached(*methods) options = extract_options(methods) cache = {} methods.each do |m| (self.is_a?(Module) ? self : self.singleton_class).class_eval do define_method("#{m}_with_cached") do |*args| if options[:timeout] if cache.has_key?(args) && cache[args][1] > Time.now cache[args] else res = send("#{m}_without_cached", *args) cache[args] = [ res, Time.now + options[:timeout] ] end else if cache.has_key?(args) cache[args] else cache[args] = send("#{m}_without_cached", *args) end end end alias_method_chain m, 'cached' end end end end Module.class_eval do include MethodModifiers end # пример использования # закэшируем два метода с устареванием закэшированных значений через 1000 секунд make_cached :find_user, find_post, :time_out => 1000
Полезные опции
- :limit => N максимальный размер кеша
- :cache_if => lambda{|args,value| .. } условие на то, кэшировать или нет значение
- :timeout => 2.hours время устаревания закешированых значений
- вызов make_cached :foo, ... должен создавать метод expire_foo, который получает либо :args => args, либо :value => value, либо блок {|args, value| ... } которые возвращает true для тех пар, которые нужно удалить из кеша