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

Yak shaving is best shaving

Yak shaving is best shaving

Madison Ruby 2013

Aaron Patterson

August 25, 2013
Tweet

More Decks by Aaron Patterson

Other Decks in Technology

Transcript

  1. AT&T, AT&T logo and all AT&T related marks are trademarks

    of AT&T Intellectual Property and/or AT&T affiliated companies.
  2. AR Methods begin User.instance_method(:name) rescue NameError puts "no method found!"

    end User.create! :name => 'foo' p User.instance_method :name
  3. source_location def define_method_attribute(name) safe_name = name.unpack('h*').first generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__

    + 1 def __temp__#{safe_name} read_attribute(:'#{name.inspect}') { |n| missing_attribute(n, caller) } end alias_method #{name.inspect}, :__temp__#{safe_name} undef_method :__temp__#{safe_name} STR end
  4. source_location def define_method_attribute(name) safe_name = name.unpack('h*').first generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__

    + 1 def __temp__#{safe_name} read_attribute(:'#{name.inspect}') { |n| missing_attribute(n, caller) } end alias_method #{name.inspect}, :__temp__#{safe_name} undef_method :__temp__#{safe_name} STR end original name
  5. source_location def define_method_attribute(name) safe_name = name.unpack('h*').first generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__

    + 1 def __temp__#{safe_name} read_attribute(:'#{name.inspect}') { |n| missing_attribute(n, caller) } end alias_method #{name.inspect}, :__temp__#{safe_name} undef_method :__temp__#{safe_name} STR end real name
  6. Under the hood >> insns = RubyVM::InstructionSequence.new "read_attribute(:name)" => <RubyVM::InstructionSequence:<compiled>@<compiled>>

    >> puts insns.disasm; nil == disasm: <RubyVM::InstructionSequence:<compiled>@<compiled>>========== 0000 trace 1 ( 1) 0002 putself 0003 putobject :name 0005 opt_send_simple <callinfo!mid:read_attribute, argc:1, FCALL|ARGS_SKIP> 0007 leave SYM B O L
  7. Under the hood >> insns = RubyVM::InstructionSequence.new "read_attribute('name')" => <RubyVM::InstructionSequence:<compiled>@<compiled>>

    >> puts insns.disasm; nil == disasm: <RubyVM::InstructionSequence:<compiled>@<compiled>>========== 0000 trace 1 ( 1) 0002 putself 0003 putstring "name" 0005 opt_send_simple <callinfo!mid:read_attribute, argc:1, FCALL|ARGS_SKIP> 0007 leave STR IN G
  8. Instruction diff >> a = RubyVM::InstructionSequence.new( ?> 'read_attribute("name")') => <RubyVM::InstructionSequence:<compiled>@<compiled>>

    >> b = RubyVM::InstructionSequence.new( ?> 'read_attribute(:"name")') => <RubyVM::InstructionSequence:<compiled>@<compiled>> >> a.to_a.last - b.to_a.last => [[:putstring, "name"]] >> b.to_a.last - a.to_a.last => [[:putobject, :name]]
  9. putstring putstring (VALUE str) () (VALUE val) { val =

    rb_str_resurrect(str); } insns.def
  10. >> 10.times { ?> puts "foo".object_id >> } 70351655497760 70351655497540

    70351655497360 70351655497300 70351655497120 70351655496900 70351655496760 70351655496640 70351655496380 70351655496320
  11. Under the hood >> ins = RubyVM::InstructionSequence.new 'read_attribute(CONSTANT)' => <RubyVM::InstructionSequence:<compiled>@<compiled>>

    >> puts ins.disasm; nil == disasm: <RubyVM::InstructionSequence:<compiled>@<compiled>>========== 0000 trace 1 ( 1) 0002 putself 0003 getinlinecache 10, <ic:0> 0006 getconstant :CONSTANT 0008 setinlinecache <ic:0> 0010 opt_send_simple <callinfo!mid:read_attribute, argc:1, FCALL|ARGS_SKIP> 0012 leave C O N STA N T
  12. getconstant getconstant (ID id) (VALUE klass) (VALUE val) { val

    = vm_get_ev_const(th, GET_ISEQ(), klass, id, 0); } insns.def
  13. No new allocations >> NAME = "foo" => "foo" >>

    10.times { puts NAME.object_id } 70351655353800 70351655353800 70351655353800 70351655353800 70351655353800 70351655353800 70351655353800 70351655353800 70351655353800 70351655353800
  14. class Thing def string read_attribute "name" end def symbol read_attribute

    :name end NAME = "name" def constant read_attribute NAME end private def read_attribute n; end end
  15. Benchmark.ips do |x| t = Thing.new x.report("constant") { t.constant }

    x.report("symbol") { t.symbol } x.report("string") { t.string } end
  16. Calculating ------------------------------------- constant 86936 i/100ms symbol 89678 i/100ms string 80625

    i/100ms ------------------------------------------------- constant 4635426.5 (±5.9%) i/s - 23124976 in 5.011890s symbol 4617404.5 (±5.7%) i/s - 23047246 in 5.011275s string 3529063.8 (±7.2%) i/s - 17576250 in 5.016706s Result
  17. We want to generate the methods via module_eval rather than

    define_method, because define_method is slower on dispatch and uses more memory (because it creates a closure).
  18. Benchmark class Thing define_method(:defnd) { } module_eval <<-eos def evald;

    end eos end Benchmark.ips do |x| t = Thing.new x.report("defnd") { t.defnd } x.report("evald") { t.evald } end
  19. Results Calculating ------------------------------------- defnd 83910 i/100ms evald 89965 i/100ms -------------------------------------------------

    defnd 4704667.3 (±6.8%) i/s - 23410890 in 5.008203s evald 6548961.6 (±6.8%) i/s - 32567330 in 5.004078s
  20. Documentation === Implementation from Module ------------------------------------------------------------------------------ define_method(symbol, method) -> new_method

    define_method(symbol) { block } -> proc ------------------------------------------------------------------------------
  21. Benchmark class Thing def foo; end define_method(:defnd) { } define_method(:defnd2,

    instance_method(:foo)) module_eval <<-eos def evald; end eos end Benchmark.ips do |x| t = Thing.new x.report("defnd") { t.defnd } x.report("defnd2") { t.defnd2 } x.report("evald") { t.evald } end
  22. Results Calculating ------------------------------------- defnd 84511 i/100ms defnd2 91485 i/100ms evald

    91643 i/100ms ------------------------------------------------- defnd 4034153.8 (±6.2%) i/s - 20113618 in 5.009757s defnd2 6549218.1 (±5.6%) i/s - 32660145 in 5.007169s evald 6522551.2 (±6.7%) i/s - 32441622 in 5.003291s
  23. We want to generate the methods via module_eval rather than

    define_method, because define_method is slower on dispatch and uses more memory (because it creates a closure).
  24. We want to generate the methods via module_eval rather than

    define_method, because define_method uses more memory (because it creates a closure).
  25. We want to generate the methods via module_eval rather than

    define_method, because define_method uses more memory (because it creates a closure).
  26. Closure reference class Thing def self.make_method thing define_method(:defnd) { }

    end end Thing.make_method nil Thing.make_method("X" * 90000)
  27. Memory test class Thing N = 100_000 if ENV['USE_EVAL'] N.times

    { |i| module_eval "def foo_#{i}; end" } else N.times { |i| define_method("foo_#{i}") { } } end end puts "DONE!" sleep
  28. Why so much? require 'objspace' class Thing N = 100_000

    if ENV['USE_EVAL'] N.times { |i| module_eval "def foo_#{i}; end" } else N.times { |i| define_method("foo_#{i}") { } } end end p ObjectSpace.memsize_of_all(RubyVM::InstructionSequence)
  29. We want to generate the methods via module_eval rather than

    define_method, because define_method uses more memory (because it creates a closure).
  30. Benchmark class Thing N = 100_000 if ENV['USE_EVAL'] N.times {

    |i| module_eval "def foo_#{i}; end" } else N.times { |i| define_method("foo_#{i}") { } } end end
  31. Result $ time USE_EVAL=1 ruby omg.rb real 0m3.629s user 0m3.530s

    sys 0m0.091s $ time ruby omg.rb real 0m0.576s user 0m0.521s sys 0m0.047s 3.6s vs 0.6s!
  32. Error $ ruby omg.rb omg.rb:6:in `define_method': bind argument must be

    a subclass of A (TypeError) from omg.rb:6:in `<class:B>' from omg.rb:5:in `<main>'
  33. Module Transplant class B methods = Module.new do define_method :name,

    AllMethods.instance_method(:name) end include methods end
  34. create_model = lambda { # Simulate many different models, but

    just share the same table # so we don't need to make many different tables Class.new(ActiveRecord::Base) { self.table_name = 'users' } } user_model = create_model.call user = user_model.create!(name: 'Aaron') x = [] growth = [10, 90, 900].map { |i| i.times { model = create_model.call x << model model.find(user.id).name } [x.size, ObjectSpace.memsize_of_all(RubyVM::InstructionSequence) - current] }
  35. Definition Speed Benchmark.ips do |x| id = user.id x.report("create model")

    do model = create_model.call model.find(id).name end end
  36. Results [aaron@higgins rails (4-0-stable)]$ bundle exec ruby omg.rb Calculating -------------------------------------

    create model 8 i/100ms ------------------------------------------------- create model 85.9 (±11.6%) i/s - 424 in 5.006028s [aaron@higgins rails (master)]$ bundle exec ruby omg.rb Calculating ------------------------------------- create model 17 i/100ms ------------------------------------------------- create model 165.5 (±13.3%) i/s - 816 in 5.020841s