Slide 1

Slide 1 text

Rinline: An inline expansion optimizar for Ruby RubyちこくKaigi 22nd, May. 2019 https://drecom.connpass.com/event/128107/

Slide 2

Slide 2 text

Agenda I created an optimizer for Ruby with inline expansion.

Slide 3

Slide 3 text

Agenda ● What is inline expansion ● What is Rinline ● How Rinline works ● The future of Rinline

Slide 4

Slide 4 text

What is inline expansion

Slide 5

Slide 5 text

What is inline expansion ● Replacing method call with body of the called method. ● It reduce overhead of method call.

Slide 6

Slide 6 text

Inline expansion example # Before def foo bar end def bar puts 42 end # After def foo puts 42 end

Slide 7

Slide 7 text

Benchmark ● `m + n` vs `1 + 2` ○ def m() 1 end; def n() 2 end ● `m + n`: 5.71s ○ x1.56 slower ● `1 + 2`: 3.66s

Slide 8

Slide 8 text

What is Rinline

Slide 9

Slide 9 text

Rinline ● Rinline is a PoC of inline expansion optimizer for Ruby ● https://github.com/pocke/rinline ● It makes Ruby faster! ○ (I hope)

Slide 10

Slide 10 text

Usage Rinline.optimize do |r| r.optimize_instance_method(Klass, :method_name) end

Slide 11

Slide 11 text

DEMO See https://github.com/pocke/rinline/blob/master/benchma rk/simple.rb

Slide 12

Slide 12 text

How Rinline works

Slide 13

Slide 13 text

Data flow

Slide 14

Slide 14 text

Data flow ● Method name → UnboundMethod → RubyVM::AST::Node → String of code → Inline expanded code → Ruby program by eval

Slide 15

Slide 15 text

Data flow example def foo bar end def bar 1 end

Slide 16

Slide 16 text

Method name → UnboundMethod ● Use Module#instance_method m = Klass.instance_method(:foo) # => #

Slide 17

Slide 17 text

UnboundMethod → RubyVM::AST::Node ● Use RubyVM::AST.of node = RubyVM::AbstractSyntaxTree.of(m) # => #

Slide 18

Slide 18 text

RubyVM::AST::Node → String of Code ● RubyVM::AST::Node#to_source ○ It defined by AstExt (refinements patch)

Slide 19

Slide 19 text

RubyVM::AST::Node → String of Code ● Use File.binread path = m.source_location[0] range = node.first_index..node.last_index source = File.binread(path)[range] # => “def foo() bar() end” note: first|last_index are defined here https://github.com/pocke/rinline/blob/2cb6ffb055c619998973f5bd2a349fcb6599e95c/lib/rinline/ext/ast_ext.rb#L144-L161

Slide 20

Slide 20 text

String of code → Inline expanded code ● Replacing method call with called method body ● I will talk it on the next section!

Slide 21

Slide 21 text

Inline expanded code → Ruby program by eval ● Use class_eval Object.class_eval(inline_expanded_code)

Slide 22

Slide 22 text

Replacing method call

Slide 23

Slide 23 text

Method call in Ruby ● FCALL → method call with arguments ■ foo(1, 2) ● VCALL → method call without arguments ■ foo ● CALL → method call with receiver ■ x.foo ● OPCALL → method call with operator ■ foo + bar

Slide 24

Slide 24 text

Rinline supports FCALL and VCALL ● Easy to support (Supporting CALL is difficult) ○ CALL has a receiver, and receiver klass is unknown statically in most cases

Slide 25

Slide 25 text

How to replace ● RubyVM::AST::Node → … → Replaced code

Slide 26

Slide 26 text

How to replace ● RubyVM::AST::Node → VCALL/FCALL Node → UnboundMethod → RubyVM::AST::Node → String of called method → lvar renamed code → Replaced code

Slide 27

Slide 27 text

Example def foo x = 1 bar p x * 3 end def bar x = 21 p x * 2 end

Slide 28

Slide 28 text

RubyVM::AST::Node → VCALL/FCALL Node ● Traverse ● Find VCALL / FCALL

Slide 29

Slide 29 text

Traverse def traverse(&block) block.call(self) children.each do |c| c.traverse(&block) if c.is_a?(RubyVM::AST::Node) end end

Slide 30

Slide 30 text

Find VCALL / FCALL def foo x = 1 bar # ← VCALL node p x * 3 end

Slide 31

Slide 31 text

VCALL/FCALL Node → UnboundMethod → Method definition Node method_name = vcall_node.children[0] # => :bar method = Klass.instance_method(method_name) method_def_node = RubyVM::AST.of(method)

Slide 32

Slide 32 text

RubyVM::AST::Node → String of called method def_src = method_def_node.to_source

Slide 33

Slide 33 text

String of called method → lvar renamed code ● Before embed, renaming lvar is needed

Slide 34

Slide 34 text

Example def foo x = 1 bar p x * 3 # => 3 end def bar x = 21 p x * 2 # => 42 end

Slide 35

Slide 35 text

Example: It will be break! def foo x = 1 x = 21 p x * 2 # => 42 p x * 3 # => 63 end def bar x = 21 p x * 2 end

Slide 36

Slide 36 text

Fix local var collision def foo x = 1 x_XXX = 21 p x_XXX * 2 # => 42 p x * 3 # => 3 end def bar x = 21 p x * 2 end

Slide 37

Slide 37 text

lvar renamed code → Replaced code def foo x = 1 x_XXX = 21 p x_XXX * 2 p x * 3 end It is ok, but actually the replaced code will be enclosed with double braces

Slide 38

Slide 38 text

lvar renamed code → Replaced code def foo x = 1 ((x_XXX = 21 p x_XXX * 2)) p x * 3 end

Slide 39

Slide 39 text

Why braces bar * 3 # => 9 def bar 1 + 2 end

Slide 40

Slide 40 text

Why braces bar * 3 # => 9 1 + 2 * 3 # => 7 (1 + 2) * 3 # => 9 def bar 1 + 2 end

Slide 41

Slide 41 text

Why double braces p bar def bar 1; 2 end

Slide 42

Slide 42 text

Why double braces p (1; 2) # => SyntaxError! p ((1; 2)) # => Valid def bar 1; 2 end

Slide 43

Slide 43 text

The future of Rinline

Slide 44

Slide 44 text

Meaningful optimization ● Currently it makes micro benchmark faster ● But it does not make actual app faster ○ For example, I tried to optimize optcarrot, but it does not change the FPS

Slide 45

Slide 45 text

Fix many bugs ● Rinline generates invalid code ○ For example: It breaks on define_method

Slide 46

Slide 46 text

Support super ● Currently, Rinline just ignores `super` node ● It can support `super` with UnboundMethod#super_method ○ https://twitter.com/udzura/status/11303947244 58565633

Slide 47

Slide 47 text

Conclusion

Slide 48

Slide 48 text

Conclusion ● RubyVM::AST.of is useful for “Dark Power” ● Rinline is a PoC of inline expansion for Ruby

Slide 49

Slide 49 text

pp self ● Masataka “pocke” Kuwabara ● Work for Bit Journey, Inc. Thank you for listening!