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

RubyKaigi 2025: Class New, A New Approach

Avatar for Aaron Patterson Aaron Patterson
May 15, 2025
3

RubyKaigi 2025: Class New, A New Approach

This talk is about optimizing Class#new. I gave the talk at RubyKaigi 2025

Avatar for Aaron Patterson

Aaron Patterson

May 15, 2025
Tweet

Transcript

  1. class Class def new(...) allocate.initialize(...) end end class Foo def

    initialize(a, b) :hello end end Foo.new(1, 2) # => ??? :hello Possible Implementation First, buggy implementation
  2. Possible Implementation Second, less buggy implementation class Class def new(...)

    obj = allocate obj.initialize(...) obj end end class Foo def initialize(a, b) :hello end end Foo.new(1, 2) # => #<Foo ...>
  3. Possible Implementation Things we need to know about class Class

    def new(...) obj = allocate obj.initialize(...) obj end end class Foo def initialize(a, b) :hello end end Foo.new(1, 2) # => #<Foo ...> method call method call 2 inline caches Calling conventions
  4. Actual Implementation Class#new implementation in C VALUE rb_class_new_instance_pass_kw(int argc, const

    VALUE *argv, VALUE klass) { VALUE obj; obj = rb_class_alloc(klass); rb_obj_call_init_kw(obj, argc, argv, RB_PASS_CALLED_KEYWORDS); return obj; } class Foo def initialize(a, b) :hello end end Foo.new(1, 2) # => #<Foo ...> VALUE rb_class_new_instance_pass_kw(int argc, const VALUE *argv, VALUE klass) { VALUE obj; obj = rb_class_alloc(klass); rb_obj_call_init_kw(obj, argc, argv, RB_PASS_CALLED_KEYWORDS); return obj; } class Foo def initialize(a, b) :hello end end Foo.new(1, 2) # => #<Foo ...> Allocation happens here
  5. VALUE rb_class_new_instance_pass_kw(int argc, const VALUE *argv, VALUE klass) { VALUE

    obj; obj = rb_class_alloc(klass); rb_obj_call_init_kw(obj, argc, argv, RB_PASS_CALLED_KEYWORDS); return obj; } class Foo def initialize(a, b) :hello end end Foo.new(1, 2) # => #<Foo ...> Actual Implementation Class#new implementation in C
  6. VALUE rb_class_new_instance_pass_kw(int argc, const VALUE *argv, VALUE klass) { VALUE

    obj; obj = rb_class_alloc(klass); rb_obj_call_init_kw(obj, argc, argv, RB_PASS_CALLED_KEYWORDS); return obj; } class Foo def initialize(a, b) :hello end end Foo.new(1, 2) # => #<Foo ...> Actual Implementation Class#new implementation in C
  7. VALUE rb_class_new_instance_pass_kw(int argc, const VALUE *argv, VALUE klass) { VALUE

    obj; obj = rb_class_alloc(klass); rb_obj_call_init_kw(obj, argc, argv, RB_PASS_CALLED_KEYWORDS); return obj; } class Foo def initialize(a, b) :hello end end Foo.new(1, 2) # => #<Foo ...> Actual Implementation Class#new implementation in C
  8. VALUE rb_class_new_instance_pass_kw(int argc, const VALUE *argv, VALUE klass) { VALUE

    obj; obj = rb_class_alloc(klass); rb_obj_call_init_kw(obj, argc, argv, RB_PASS_CALLED_KEYWORDS); return obj; } class Foo def initialize(a, b) :hello end end Foo.new(1, 2) # => #<Foo ...> Actual Implementation Class#new implementation in C C Ruby Ruby
  9. Different Calling Conventions Crossing language border requires translation C Ruby

    Ruby Translate to C calling convention Translate back to Ruby calling convention
  10. Where is time spent? Arrows are impacted by calling convention

    C Ruby Ruby Ruby Ruby Ruby Time Savings
  11. Where is time spent? Methods are impacted by VM speed

    / method lookup (inline caches) C Ruby Ruby Ruby Ruby Ruby Time Savings
  12. Where is “baz”? Methods must be located, and we can

    cache the location class Foo def bar self.baz end def baz end end Where is the baz method?
  13. Where is “baz”? Method lookup routine def find_method(receiver, method_name) #

    Loop through ancestors until we find the method while !receiver.method_defined?(method_name) receiver = receiver.superclass end # Return the method receiver.method(method_name) end def call_method(receiver, method_name) # Call the method find_method(receiver, method_name).call end
  14. Method lookup can be linear We have to scan ancestors

    looking for the method class A; def baz; end end class B < A; end class C < B; end class D < C; end class E < D; end class F < E; end # .... class Foo < Z def bar self.baz end end 😭
  15. Receiver type is our cache key Create a cache entry

    where the key is the class, and the value is the method class A; def baz; end end class B < A; end class C < B; end class D < C; end class E < D; end class F < E; end # .... class Foo < Z def bar self.baz end end Cache key is the class of self: Foo Cache value is the method entry
  16. Cache: Stored inline with bytecode That’s why it’s called “inline”

    cache == disasm: #<ISeq:[email protected]:11 (11,2)-(13,5)> 0000 putself ( 12)[LiCa] 0001 opt_send_without_block <calldata!mid:baz, argc:0, FCALL|ARGS_SIMPLE> 0003 leave ( 13)[Re]
  17. Cache: Stored inline with bytecode That’s why it’s called “inline”

    cache == disasm: #<ISeq:[email protected]:11 (11,2)-(13,5)> 0000 putself ( 12)[LiCa] 0001 opt_send_without_block <calldata!mid:baz, argc:0, FCALL|ARGS_SIMPLE> 0003 leave ( 13)[Re]
  18. Inline Cache Object Graph bar method points at cache, cache

    points at method entry class A; def baz; end end class B < A; end class C < B; end class D < C; end class E < D; end class F < E; end # .... class Foo < Z def bar self.baz end end bar method A#baz inline cache A#baz method Weak Reference Ruby Objects class A
  19. Measure In line Cache Allocations Objects can be allocated on

    the fi rst call, but not subsequent def baz; end def bar # First time this is called, an object gets allocated self.baz end def measure x = GC.stat(:total_allocated_objects) yield GC.stat(:total_allocated_objects) - x end measure { } # heat p measure { bar } # => 2 p measure { bar } # => 0 First call allocates
  20. Inline Cache: Only holds one object Inline caches are “monomorphic”,

    they only cache one item class A; def baz; end end class B < A; end class C < B; end class D < C; end class E < D; end class F < E; end # .... class Foo < Z def bar self.baz end end bar method A#baz inline cache A#baz method Weak Reference class A
  21. Inline Cache: Only holds one object Inline caches are “monomorphic”,

    they only cache one item class A def bar; end end class B def bar; end end def run_it(obj); obj.bar; end run_it(A.new) run_it(B.new) run_it method A#bar inline cache A#bar method B#bar inline cache B#bar method
  22. Cache hit / miss examples Cache size is one, so

    we only hit when repeating types def run_it(obj); obj.bar; end run_it(A.new) # cache miss run_it(A.new) # cache hit run_it(A.new) # cache hit run_it(A.new) # cache hit run_it(B.new) # cache miss run_it(B.new) # cache hit run_it(B.new) # cache hit run_it(B.new) # cache hit run_it(A.new) # cache miss run_it(B.new) # cache miss run_it(A.new) # cache miss run_it(B.new) # cache miss run_it(A.new) # cache miss run_it(B.new) # cache miss
  23. Cache hit / miss comparison Compare always hitting to never

    hitting class A; def bar; end; end class B; def bar; end; end def run_it(obj); obj.bar; end a = A.new b = B.new Benchmark.ips { |x| x.report("always hit") { run_it(a); run_it(a); run_it(a); run_it(a) run_it(a); run_it(a); run_it(a); run_it(a) run_it(a); run_it(a); run_it(a); run_it(a) run_it(a); run_it(a); run_it(a); run_it(a) } x.report("never hit") { run_it(a); run_it(b); run_it(a); run_it(b) run_it(a); run_it(b); run_it(a); run_it(b) run_it(a); run_it(b); run_it(a); run_it(b) run_it(a); run_it(b); run_it(a); run_it(b) } x.compare! } always call with a alternate between a and b
  24. Benchmark Results Never hitting is about 40% slower $ ruby

    test.rb ruby 3.4.1 (2024-12-25 revision 48d4efcb85) +PRISM [arm64-darwin24] Warming up -------------------------------------- always hit 319.277k i/100ms never hit 224.288k i/100ms Calculating ------------------------------------- always hit 3.189M (± 2.4%) i/s (313.58 ns/i) - 15.964M in 5.008714s never hit 2.280M (± 1.8%) i/s (438.55 ns/i) - 11.439M in 5.018054s Comparison: always hit: 3188993.5 i/s never hit: 2280264.0 i/s - 1.40x slower $ ruby test.rb ruby 3.4.1 (2024-12-25 revision 48d4efcb85) +PRISM [arm64-darwin24] Warming up -------------------------------------- always hit 319.277k i/100ms never hit 224.288k i/100ms Calculating ------------------------------------- always hit 3.189M (± 2.4%) i/s (313.58 ns/i) - 15.964M in 5.008714s never hit 2.280M (± 1.8%) i/s (438.55 ns/i) - 11.439M in 5.018054s Comparison: always hit: 3188993.5 i/s never hit: 2280264.0 i/s - 1.40x slower
  25. VALUE rb_class_new_instance_pass_kw(int argc, const VALUE *argv, VALUE klass) { VALUE

    obj; obj = rb_class_alloc(klass); rb_obj_call_init_kw(obj, argc, argv, RB_PASS_CALLED_KEYWORDS); return obj; } class Foo def initialize(a, b) :hello end end Foo.new(1, 2) # => #<Foo ...> Initialize is called from C Class#new implementation in C
  26. rb_obj_call_init_kw Calls your initialize method void rb_obj_call_init_kw(VALUE obj, int argc,

    const VALUE *argv, int kw_splat) { PASS_PASSED_BLOCK_HANDLER(); rb_funcallv_kw(obj, idInitialize, argc, argv, kw_splat); } Call a m ethod O n this object M ethod nam e Param eters
  27. rb_funcallv_kw Calls any method VALUE rb_funcallv_kw(VALUE recv, ID mid, int

    argc, const VALUE *argv, int kw_splat) { VM_ASSERT(ruby_thread_has_gvl_p()); return rb_call(recv, mid, argc, argv, kw_splat ? CALL_FCALL_KW : CALL_FCALL); } VALUE rb_funcallv_kw(VALUE recv, ID mid, int argc, const VALUE *argv, int kw_splat) { VM_ASSERT(ruby_thread_has_gvl_p()); return rb_call(recv, mid, argc, argv, kw_splat ? CALL_FCALL_KW : CALL_FCALL); }
  28. rb_call Just passes everything to rb_call0 static inline VALUE rb_call(VALUE

    recv, ID mid, int argc, const VALUE *argv, call_type scope) { rb_execution_context_t *ec = GET_EC(); return rb_call0(ec, recv, mid, argc, argv, scope, ec->cfp->self); } static inline VALUE rb_call(VALUE recv, ID mid, int argc, const VALUE *argv, call_type scope) { rb_execution_context_t *ec = GET_EC(); return rb_call0(ec, recv, mid, argc, argv, scope, ec->cfp->self); } Why is it named rb_call0????? 😂
  29. rb_call0 Is actually complicated, and I’ve omitted some of it

    static inline VALUE rb_call0(rb_execution_context_t *ec, VALUE recv, ID mid, int argc, const VALUE *argv, call_type call_scope, VALUE self) { /* Snip */ struct rb_callinfo ci; scope_to_ci(scope, mid, argc, &ci); const struct rb_callcache *cc = gccct_method_search(ec, recv, mid, &ci); /* Snip */ return vm_call0_cc(ec, recv, mid, argc, argv, cc, kw_splat); } static inline VALUE rb_call0(rb_execution_context_t *ec, VALUE recv, ID mid, int argc, const VALUE *argv, call_type call_scope, VALUE self) { /* Snip */ struct rb_callinfo ci; scope_to_ci(scope, mid, argc, &ci); const struct rb_callcache *cc = gccct_method_search(ec, recv, mid, &ci); /* Snip */ return vm_call0_cc(ec, recv, mid, argc, argv, cc, kw_splat); }
  30. gccct_method_search gccct: Global call cache cache table static inline const

    struct rb_callcache * gccct_method_search(rb_execution_context_t *ec, VALUE recv, ID mid, const struct rb_callinfo *ci) { /* snip */ // search global method cache unsigned int index = (unsigned int)(gccct_hash(klass, mid) % VM_GLOBAL_CC_CACHE_TABLE_SIZE); rb_vm_t *vm = rb_ec_vm_ptr(ec); const struct rb_callcache *cc = vm->global_cc_cache_table[index]; if (LIKELY(cc)) { if (LIKELY(vm_cc_class_check(cc, klass))) { const rb_callable_method_entry_t *cme = vm_cc_cme(cc); if (LIKELY(!METHOD_ENTRY_INVALIDATED(cme) && cme->called_id == mid)) { VM_ASSERT(vm_cc_check_cme(cc, rb_callable_method_entry(klass, mid))); return cc; } } } return gccct_method_search_slowpath(vm, klass, index, ci); } static inline const struct rb_callcache * gccct_method_search(rb_execution_context_t *ec, VALUE recv, ID mid, const struct rb_callinfo *ci) { /* snip */ // search global method cache unsigned int index = (unsigned int)(gccct_hash(klass, mid) % VM_GLOBAL_CC_CACHE_TABLE_SIZE); rb_vm_t *vm = rb_ec_vm_ptr(ec); const struct rb_callcache *cc = vm->global_cc_cache_table[index]; if (LIKELY(cc)) { if (LIKELY(vm_cc_class_check(cc, klass))) { const rb_callable_method_entry_t *cme = vm_cc_cme(cc); if (LIKELY(!METHOD_ENTRY_INVALIDATED(cme) && cme->called_id == mid)) { VM_ASSERT(vm_cc_check_cme(cc, rb_callable_method_entry(klass, mid))); return cc; } } } return gccct_method_search_slowpath(vm, klass, index, ci); } static inline const struct rb_callcache * gccct_method_search(rb_execution_context_t *ec, VALUE recv, ID mid, const struct rb_callinfo *ci) { /* snip */ // search global method cache unsigned int index = (unsigned int)(gccct_hash(klass, mid) % VM_GLOBAL_CC_CACHE_TABLE_SIZE); rb_vm_t *vm = rb_ec_vm_ptr(ec); const struct rb_callcache *cc = vm->global_cc_cache_table[index]; if (LIKELY(cc)) { if (LIKELY(vm_cc_class_check(cc, klass))) { const rb_callable_method_entry_t *cme = vm_cc_cme(cc); if (LIKELY(!METHOD_ENTRY_INVALIDATED(cme) && cme->called_id == mid)) { VM_ASSERT(vm_cc_check_cme(cc, rb_callable_method_entry(klass, mid))); return cc; } } } return gccct_method_search_slowpath(vm, klass, index, ci); } static inline const struct rb_callcache * gccct_method_search(rb_execution_context_t *ec, VALUE recv, ID mid, const struct rb_callinfo *ci) { /* snip */ // search global method cache unsigned int index = (unsigned int)(gccct_hash(klass, mid) % VM_GLOBAL_CC_CACHE_TABLE_SIZE); rb_vm_t *vm = rb_ec_vm_ptr(ec); const struct rb_callcache *cc = vm->global_cc_cache_table[index]; if (LIKELY(cc)) { if (LIKELY(vm_cc_class_check(cc, klass))) { const rb_callable_method_entry_t *cme = vm_cc_cme(cc); if (LIKELY(!METHOD_ENTRY_INVALIDATED(cme) && cme->called_id == mid)) { VM_ASSERT(vm_cc_check_cme(cc, rb_callable_method_entry(klass, mid))); return cc; } } } return gccct_method_search_slowpath(vm, klass, index, ci); } Lookup cache in global table Is the cache entry good? Slow path
  31. C methods calling Ruby methods It has “method caches” Stored

    in a global table Cache entries limited by table size
  32. A “convention” for connect methods Calling convention de fi nes

    where parameters and return values will be def bar(x, y, z) x + y + z end def foo bar(1, 2, 3) end Arguments are stored in a certain place Parameters are read from a certain place Return value is stored in a certain place Return value is read from a certain place
  33. def bar(a, b, c) a + b + c end

    def foo bar(5, 7, 9) end Ruby Code Calling Convention: Caller’s Side Stack values become method parameters def bar(a, b, c) a + b + c end def foo bar(5, 7, 9) end Ruby Code Push 5 Instructions Stack 5 Push 7 Push 9 7 9 Call bar
  34. def bar(a, b, c) a + b + c end

    def foo bar(5, 7, 9) end Ruby Code Calling Convention: Callee’s side Stack values become method parameters Instructions Stack 5 Getlocal -3 Getlocal -2 Add Getlocal -1 Add 7 9 5 12 21 Return 7 9
  35. Calling Convention: Keyword Arguments Stack values become method parameters def

    bar(a:, b:, c:) a + b + c end def foo bar(a: 5, b: 7, c: 9) end Ruby Code Instructions Stack 5 Getlocal -3 Getlocal -2 Add Getlocal -1 Add 7 9 5 12 21 Return
  36. Calling Convention: Keyword Arguments Stack values become method parameters def

    bar(a:, b:, c:) a + b + c end def foo bar(c: 9, a: 5, b: 7) end Ruby Code Instructions Stack 9 Push 9 Push 5 Push 7 Call bar 5 7
  37. Keyword Arguments Normally don’t require allocations def bar(a:, b:, c:)

    a + b + c end def foo bar(c: 9, a: 5, b: 7) end count_allocs { foo } # heat p count_allocs { foo } # => 0 Ruby Ruby
  38. Keyword Arguments + Initialize We expect one allocation, Foo, but

    there are two allocations class Foo def initialize(a:, b:, c:) end end def foo Foo.new(c: 9, a: 5, b: 7) end count_allocs { foo } # heat p count_allocs { foo } # => 2 Ruby C Ruby
  39. Hash is allocated across language barrier We have to convert

    kwargs to a hash, then back to stack positions 5 7 9 { a: 5, b: 7, c: 9 } 5 7 9
  40. Class#new First implementation class Class def new(...) instance = allocate

    instance.initialize(...) instance end end Initialize is private!
  41. Trying to call Initialize won’t work Because it’s a private

    method 😭 class Foo def initialize end private def bar; end end Foo.allocate.initialize $ ruby thing.rb thing.rb:10:in '<main>': private method 'initialize' called for an instance of Foo (NoMethodError) Foo.allocate.initialize ^^^^^^^^^^^
  42. Class#new, second attempt Also doesn’t work class Class def new(...)

    instance = allocate instance.send(:initialize, ...) instance end end No “send” on BasicObject
  43. BasicObject is very Basic It doesn’t support send! obj =

    BasicObject.new obj.send :initialize $ ruby thing.rb thing.rb:2:in '<main>': undefined method 'send' for an instance of BasicObject (NoMethodError) obj.send :initialize ^^^^^
  44. Special flags on send instruction FCALL means “ignore method visibility”

    class Foo def initialize bar end private def bar; end end Ruby Code == disasm: #<ISeq:[email protected]:2 (2,2)-(4,5)> 0000 putself ( 3)[LiCa] 0001 send <calldata!mid:bar, argc:0, FCALL|VCALL|ARGS_SIMPLE>, nil 0004 leave ( 4)[Re] VM Instructions == disasm: #<ISeq:[email protected]:2 (2,2)-(4,5)> 0000 putself ( 3)[LiCa] 0001 send <calldata!mid:bar, argc:0, FCALL|VCALL|ARGS_SIMPLE>, nil 0004 leave ( 4)[Re] VM Instructions
  45. Class#new Instructions We need an FCALL fl ag == disasm:

    #<ISeq:[email protected]:2 (2,2)-(6,5)> local table (size: 2, argc: 0 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1]) [ 2] "..."@0 [ 1] instance@1 0000 putself ( 3)[LiCa] 0001 send <calldata!mid:allocate, argc:0, FCALL|VCALL|ARGS_SIMPLE>, nil 0004 setlocal instance@1, 0 0007 getlocal instance@1, 0 ( 4)[Li] 0010 getlocal "..."@0, 0 0013 sendforward <calldata!mid:initialize, argc:0, FORWARDING>, nil 0016 pop 0017 getlocal instance@1, 0 ( 5)[Li] 0020 leave ( 6)[Re] == disasm: #<ISeq:[email protected]:2 (2,2)-(6,5)> local table (size: 2, argc: 0 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1]) [ 2] "..."@0 [ 1] instance@1 0000 putself ( 3)[LiCa] 0001 send <calldata!mid:allocate, argc:0, FCALL|VCALL|ARGS_SIMPLE>, nil 0004 setlocal instance@1, 0 0007 getlocal instance@1, 0 ( 4)[Li] 0010 getlocal "..."@0, 0 0013 sendforward <calldata!mid:initialize, argc:0, FORWARDING>, nil 0016 pop 0017 getlocal instance@1, 0 ( 5)[Li] 0020 leave ( 6)[Re] Add FCALL here
  46. Class#new take three With a primitive class Class def new(...)

    obj = allocate Primitive.send_delegate!( obj, :initialize, ...) obj end end
  47. == disasm: #<ISeq:new@<internal:class>:2 (2,2)-(8,5)> local table (size: 2, argc: 0

    [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1]) [ 2] "..."@0 [ 1] obj@1 0000 putself ( 3)[LiCa] 0001 opt_send_without_block <calldata!mid:allocate, argc:0, FCALL|VCALL|ARGS_SIMPLE> 0003 setlocal_WC_0 obj@1 0005 getlocal_WC_0 obj@1 ( 5)[Li] 0007 getlocal_WC_0 "..."@0 ( 4) 0009 sendforward <calldata!mid:initialize, argc:0, FCALL|FORWARDING>, nil 0012 pop 0013 getlocal_WC_0 obj@1 ( 7)[Li] 0015 leave ( 8)[Re] Class#new take 3 Instructions == disasm: #<ISeq:new@<internal:class>:2 (2,2)-(8,5)> local table (size: 2, argc: 0 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1]) [ 2] "..."@0 [ 1] obj@1 0000 putself ( 3)[LiCa] 0001 opt_send_without_block <calldata!mid:allocate, argc:0, FCALL|VCALL|ARGS_SIMPLE> 0003 setlocal_WC_0 obj@1 0005 getlocal_WC_0 obj@1 ( 5)[Li] 0007 getlocal_WC_0 "..."@0 ( 4) 0009 sendforward <calldata!mid:initialize, argc:0, FCALL|FORWARDING>, nil 0012 pop 0013 getlocal_WC_0 obj@1 ( 7)[Li] 0015 leave ( 8)[Re] FCALL
  48. Class#new take three Doesn’t pass tests class Class def new(...)

    obj = allocate Primitive.send_delegate!( obj, :initialize, ...) obj end end People monkey patch allocate
  49. Monkeypatch to allocate is ignored This code works fi ne

    class Class def allocate raise "hahahaha" end end class Foo end Foo.new # works fine
  50. Allocate an object without calling “allocate” Primitive is not impacted

    by monkey patches class Class def new(...) obj = Primitive.rb_class_alloc2 Primitive.send_delegate!( obj, :initialize, ...) obj end end class Class def new(...) obj = Primitive.rb_class_alloc2 Primitive.send_delegate!( obj, :initialize, ...) obj end end
  51. Class#new instruction sequence Allocating a new object: 8 instructions obj

    = Primitive.rb_class_alloc2 Primitive.send_delegate!( obj, :initialize, ...) obj Ruby Code allocate Instructions Stack setlocal getlocal getlocal send pop getlocal leave new instance ...
  52. Ruby Code is YARV Our Ruby code is converted to

    YARV instructions class Class def new(...) obj = Primitive.rb_class_alloc2 Primitive.send_delegate!( obj, :initialize, ...) obj end end Ruby Code YARV Instructions allocate send pop leave dup Foo.new Ruby Code putobject Foo send
  53. YARV Instructions Paste “Class#new” code at the call site allocate

    send pop dup Foo.new Ruby Code putobject Foo
  54. Compiler Modifications - PUSH_SEND_R(ret, location, method_id, INT2FIX(orig_argc), block_iseq, INT2FIX(flags), kw_arg);

    + LABEL *not_basic_new = NEW_LABEL(location.line); + LABEL *not_basic_new_finish = NEW_LABEL(location.line); + + bool inline_new = ISEQ_COMPILE_DATA(iseq)->option->specialized_instruction && + method_id == rb_intern("new") && + call_node->block == NULL && + !(flags & VM_CALL_ARGS_BLOCKARG); + + if (inline_new) { + if (LAST_ELEMENT(ret) == opt_new_prelude) { + PUSH_INSN(ret, location, putnil); + PUSH_INSN(ret, location, swap); + } + else { + ELEM_INSERT_NEXT(opt_new_prelude, &new_insn_body(iseq, location.line, location.node_id, BIN(swap), 0)->link); + ELEM_INSERT_NEXT(opt_new_prelude, &new_insn_body(iseq, location.line, location.node_id, BIN(putnil), 0)->link); + } + + // Jump unless the receiver uses the "basic" implementation of "new" + VALUE ci; + if (flags & VM_CALL_FORWARDING) { + ci = (VALUE)new_callinfo(iseq, method_id, orig_argc + 1, flags, kw_arg, 0); + } + else { + ci = (VALUE)new_callinfo(iseq, method_id, orig_argc, flags, kw_arg, 0); + } + + PUSH_INSN2(ret, location, opt_new, ci, not_basic_new); + LABEL_REF(not_basic_new); + // optimized path + PUSH_SEND_R(ret, location, rb_intern("initialize"), INT2FIX(orig_argc), block_iseq, INT2FIX(flags | VM_CALL_FCALL), kw_arg); + PUSH_INSNL(ret, location, jump, not_basic_new_finish); + Is this a call to “new”? Insert special instructions If not new, jump to slow path
  55. “Object.new” Before and after inlining Instructions for “new” are inserted

    at the call site > ruby --dump=insns -e'Object.new' == disasm: #<ISeq:<main>@-e:1 (1,0)-(1,10)> 0000 opt_getconstant_path <ic:0 Object> ( 1)[Li] 0002 opt_send_without_block <calldata!mid:new, argc:0, ARGS_SIMPLE> 0004 leave Before > ./ruby --dump=insns -e'Object.new' == disasm: #<ISeq:<main>@-e:1 (1,0)-(1,10)> 0000 opt_getconstant_path <ic:0 Object> ( 1)[Li] 0002 putnil 0003 swap 0004 opt_new <calldata!mid:new, argc:0, ARGS_SIMPLE>, 11 0007 opt_send_without_block <calldata!mid:initialize, argc:0, FCALL|ARGS_SIMPLE> 0009 jump 14 0011 opt_send_without_block <calldata!mid:new, argc:0, ARGS_SIMPLE> 0013 swap 0014 pop 0015 leave After
  56. Keyword Arguments Only one object allocated class Foo def initialize(a:,

    b:, c:) end end def foo Foo.new(c: 9, a: 5, b: 7) end count_allocs { foo } # heat p count_allocs { foo } # => 1 Only One Allocation!!!
  57. Positional Parameters Allocations per Second by Ruby version Allocations Per

    Second 0 9500000 19000000 28500000 38000000 Number of Parameters 0 1 2 3 4 5 6 7 8 9 10 Ruby 3.5 + inlining Ruby 3.4
  58. Keyword Parameters Allocations per second by Ruby version Allocations Per

    Second 0 10000000 20000000 30000000 40000000 Number of Parameters 0 1 2 3 4 5 6 7 8 9 10 Ruby 3.5+inlining Ruby 3.4
  59. Positional Parameters + Varied Classes Allocations per second by Ruby

    version
 (varying allocated class) Allocations per Second 0 9500000 19000000 28500000 38000000 Number of Parameters 0 1 2 3 4 5 6 7 8 9 10 Ruby 3.5+Inlining Ruby 3.4
  60. Keyword Parameters + Varied Classes Allocations per second by Ruby

    version
 (varying allocated class) Allocations Per Second 0 10000000 20000000 30000000 40000000 Number of Parameters 0 1 2 3 4 5 6 7 8 9 10 Ruby 3.5+Inlining Ruby 3.4
  61. Measure ISeq size How many bytes does the “alloc” method

    use? require "objspace" def alloc Object.new end m = method(:alloc) insn = RubyVM::InstructionSequence.of(insn) puts ObjectSpace.memsize_of(insn) Ruby 3.5 + inlining: 656 bytes Ruby 3.4: 544 bytes +122 Bytes
  62. Stack Trace is Different Class#new is missing class Foo def

    initialize puts caller end end def hello Foo.new end hello > ruby test.rb test.rb:8:in 'Class#new' test.rb:8:in 'Object#hello' test.rb:11:in '<main>' Ruby 3.4 > ./ruby test.rb test.rb:8:in 'Object#hello' test.rb:11:in '<main>' Ruby 3.5 + inlining