Slide 1

Slide 1 text

Bindings in Ruby behind the music magic of blocks

Slide 2

Slide 2 text

About me Piotr Szmielew http://github.com/esse http://piotr.szmielew.pl @essepl on twitter In Rails since 2009

Slide 3

Slide 3 text

a = 2 10.times do a += 1 end puts a

Slide 4

Slide 4 text

a = 2 10.times do a += 1 end puts a # => 12

Slide 5

Slide 5 text

Obvious?

Slide 6

Slide 6 text

Now harder:

Slide 7

Slide 7 text

class BlockTest def self.add_method local = 2 define_method : do local end end end b = BlockTest.new BlockTest.add_method b.

Slide 8

Slide 8 text

Will it work?

Slide 9

Slide 9 text

class BlockTest def self.add_method local = 2 define_method : do local end end end b = BlockTest.new BlockTest.add_method b. # => 2

Slide 10

Slide 10 text

No content

Slide 11

Slide 11 text

No content

Slide 12

Slide 12 text

No content

Slide 13

Slide 13 text

No content

Slide 14

Slide 14 text

Code (lambda) environment Then - what is a closure?

Slide 15

Slide 15 text

p = proc do a = 2 10.times do a += 1 end end puts RubyVM::InstructionSequence.disasm(p)

Slide 16

Slide 16 text

No content

Slide 17

Slide 17 text

== disasm: ===== 0004 putobject 2 0006 setlocal a, 3 0011 putobject 10 0013 send 0017 leave == disasm: 0004 getlocal a, 4 0007 putobject_OP_INT2FIX_O_1_C_ 0008 opt_plus 0010 dup 0011 setlocal a, 4

Slide 18

Slide 18 text

== disasm: ===== 0004 putobject 2 0006 setlocal a, 3 0011 putobject 10 0013 send 0017 leave == disasm: 0004 getlocal a, 4 0007 putobject_OP_INT2FIX_O_1_C_ 0008 opt_plus 0010 dup 0011 setlocal a, 4

Slide 19

Slide 19 text

No content

Slide 20

Slide 20 text

No content

Slide 21

Slide 21 text

Code (lambda) environment What is a closure?

Slide 22

Slide 22 text

YARV Virtual machine with stack and heap

Slide 23

Slide 23 text

YARV Current execution state – contained in stackframe Therefore local variables are on stack

Slide 24

Slide 24 text

YARV Current execution state – contained in stackframe Therefore local variables are on stack

Slide 25

Slide 25 text

Using lambda / proc changes block in normal variable (first class citizen)

Slide 26

Slide 26 text

BUT

Slide 27

Slide 27 text

No content

Slide 28

Slide 28 text

No content

Slide 29

Slide 29 text

2.2.4 :048 > binding => # 2.2.4 :049 > binding.class => Binding 2.2.4 :050 > binding.class.superclass => Object

Slide 30

Slide 30 text

Binding Object binding Instance

Slide 31

Slide 31 text

Binding is created simultaneously with lambda

Slide 32

Slide 32 text

How exactly is binding created?

Slide 33

Slide 33 text

MRI // proc.c static VALUE rb_f_binding(VALUE self) { return rb_binding_new(); } (responsible for Kernel.binding method)

Slide 34

Slide 34 text

MRI // proc.c VALUE rb_binding_new(void) { rb_thread_t *th = GET_THREAD(); return rb_vm_make_binding(th, th->cfp); }

Slide 35

Slide 35 text

Next examples are from vm.c

Slide 36

Slide 36 text

VALUE rb_vm_make_binding(rb_thread_t *th, const rb_control_frame_t *src_cfp) { (...) rb_binding_t *bind; (...) envval = vm_make_env_object(th, cfp); (...) vm_bind_update_env(bind, envval); }

Slide 37

Slide 37 text

static VALUE vm_make_env_object(rb_thread_t *th, rb_control_frame_t *cfp) { VALUE envval = vm_make_env_each(th, cfp); (...) } MRI

Slide 38

Slide 38 text

vm_make_env_each(rb_thread_t *const th, rb_control_frame_t *const cfp) { (...)

Slide 39

Slide 39 text

/* * # local variables on a stack frame (N == local_size) * [lvar1, lvar2, ..., lvarN, SPECVAL] * ^ * ep[0] * * # moved local variables * [lvar1, lvar2, ..., lvarN, SPECVAL, Envval, BlockProcval (if needed)] * ^ ^ * env->env[0] ep[0] */

Slide 40

Slide 40 text

env_size = local_size + 1 /* envval */ + (blockprocval ? 1 : 0) /* blockprocval */; env_body = ALLOC_N(VALUE, env_size); MEMCPY(env_body, ep - (local_size - 1 /* specval */), VALUE, local_size); (...) env_ep = &env_body[local_size - 1 /* specval */]; (...) env = vm_env_new(env_ep, env_body, env_size, env_iseq); (...) return (VALUE)env; }

Slide 41

Slide 41 text

Rubinius def binding return Binding.setup( Rubinius::VariableScope.of_sender, Rubinius::CompiledCode.of_sender, Rubinius::LexicalScope.of_sender, Rubinius::VariableScope.of_sender.self, Rubinius::Location.of_closest_ruby_method ) end

Slide 42

Slide 42 text

# core / binding.rb def self.setup(variables, code, lexical_scope, recv=nil, location=nil) bind = allocate() bind.receiver = self_context(recv, variables) bind.variables = variables bind.compiled_code = code bind.lexical_scope = lexical_scope bind.location = location return bind end

Slide 43

Slide 43 text

# core / variable_scope.rb module Rubinius class VariableScope (...) def self.of_sender Rubinius.primitive :variable_scope_of_sender (...) end (...) end end Rubinius

Slide 44

Slide 44 text

Rubinius // machine/class/variable_scope.cpp VariableScope* VariableScope::of_sender(STATE) { if(CallFrame* frame = state->vm()->get_ruby_frame(1)) { return frame->promote_scope(state); } return nil(); }

Slide 45

Slide 45 text

Rubinius // machine/call_frame.hpp VariableScope* promote_scope(STATE) { if(VariableScope* vs = scope->on_heap()) return vs; return promote_scope_full(state); }

Slide 46

Slide 46 text

Rubinius // machine/call_frame.cpp VariableScope* CallFrame::promote_scope_full(STATE) { return scope->create_heap_alias(state, this, !has_closed_scope_p()); }

Slide 47

Slide 47 text

// machine/stack_variable.cpp VariableScope* StackVariables::create_heap_alias(STATE, CallFrame* call_frame, bool full) { if(on_heap_) return on_heap_; (...)

Slide 48

Slide 48 text

VariableScope* scope = state->memory()->new_object(state, G(variable_scope)); scope->self(state, self_); scope->block(state, block_); scope->module(state, module_); scope->method(state, call_frame->compiled_code); scope->heap_locals(state, nil()); scope->last_match(state, last_match_); scope->fiber(state, state->vm()->thread()- >current_fiber());

Slide 49

Slide 49 text

(...) if(!full) { scope->isolated(1); scope->heap_locals(state, Tuple::create(state, mcode->number_of_locals)); for(int i = 0; i < scope- >number_of_locals(); i++) { scope->set_local(state, i, locals_[i]); } }

Slide 50

Slide 50 text

scope->locals(locals_); scope->dynamic_locals(state, nil()); on_heap_ = scope; return scope; }

Slide 51

Slide 51 text

Calling binding method results in copying variables from stackframe to heap

Slide 52

Slide 52 text

Local variables became persistent!

Slide 53

Slide 53 text

How about jRuby?

Slide 54

Slide 54 text

Binding vs RubyBinding

Slide 55

Slide 55 text

Binding /** * Internal live representation of a block ({...} or do ... end). */ public class Binding { ...

Slide 56

Slide 56 text

RubyBinding @JRubyClass(name="Binding") public class RubyBinding extends RubyObject { private Binding binding; ... }

Slide 57

Slide 57 text

jRuby // RubyKernel.java @JRubyMethod(name = "binding", ...) public static RubyBinding binding19(ThreadContext context, IRubyObject recv, Block block) { return RubyBinding.newBinding(context.runtime, context.currentBinding()); }

Slide 58

Slide 58 text

// RubyBinding.java public static RubyBinding newBinding(Ruby runtime, Binding binding) { return new RubyBinding(runtime, runtime.getBinding(), binding); } public RubyBinding(Ruby runtime, RubyClass rubyClass, Binding binding) { super(runtime, rubyClass); this.binding = binding; }

Slide 59

Slide 59 text

//RubyGlobal.java public static void createGlobals(ThreadContext context, Ruby runtime) { GlobalVariables globals = runtime.getGlobalVariables(); runtime.defineGlobalConstant("TOPLEVEL_BINDING", runtime.newBinding()); (...) }

Slide 60

Slide 60 text

//Block.java public Block(BlockBody body, Binding binding) { assert binding != null; this.body = body; this.binding = binding; }

Slide 61

Slide 61 text

//RubyEnumerable.java public static IRubyObject each(ThreadContext context, IRubyObject self, BlockBody body) { Block block = new Block(body, context.currentBinding(self, Visibility.PUBLIC)); (...) }

Slide 62

Slide 62 text

Let's go back to MRI world…

Slide 63

Slide 63 text

def get_binding a = 2 b = 3 binding end get_binding.local_variables # => [:a, :b] get_binding.local_variable_get(:a) # => 2

Slide 64

Slide 64 text

But be careful!

Slide 65

Slide 65 text

def test_lambda a = 10 l = lambda { a } a = "OH HAI" l end test_lambda.call #=> "OH HAI"

Slide 66

Slide 66 text

YARV internally copies all variables from stackframe to heap, then use them as new stackframe

Slide 67

Slide 67 text

a = 0 add = lambda { a += 1 } sub = lambda { a -= 1 } add.call sub.call add.call sub.call add.call a # => ?

Slide 68

Slide 68 text

a = 0 add = lambda { a += 1 } sub = lambda { a -= 1 } add.call sub.call add.call sub.call add.call a # => 1

Slide 69

Slide 69 text

All lamdbas are actually operating on same copy of stackframe

Slide 70

Slide 70 text

So – can we change binding we're actually working in?

Slide 71

Slide 71 text

Almost

Slide 72

Slide 72 text

def test_metody set = lambda { new_var = 10 } set.call new_var end test_metody # => NameError: undefined local variable or method `new_var' for main:Object

Slide 73

Slide 73 text

We can't do this because in original stackframe this variable wasn't present. Therefore it isn't present in current lexical scope. It is created in other lexical scope – scope of lambda.

Slide 74

Slide 74 text

And what if we call it from inside an object?

Slide 75

Slide 75 text

class BindingTest def initialize @a = 1 end def get_binding binding end end b = BindingTest.new.get_binding b.receiver # => #

Slide 76

Slide 76 text

What else can we do with binding?

Slide 77

Slide 77 text

def get_binding a = 2 b = 3 binding end eval('a + b', get_binding) # => 5

Slide 78

Slide 78 text

We can also change binding

Slide 79

Slide 79 text

def get_binding a = 2 b = 3 binding end b = get_binding eval('c = a + b', b) b.local_variable_get(:c) # => 5

Slide 80

Slide 80 text

And since binding is an object and it gives access to all local variables…

Slide 81

Slide 81 text

# lib/pry/core_extensions.rb class Object # Start a Pry REPL on self. (...) def pry(object=nil, hash={}) if object.nil? || Hash === object Pry.start(self, object || {}) else Pry.start(object, hash) end end (...) end

Slide 82

Slide 82 text

Local variables are also quite convenient in templates

Slide 83

Slide 83 text

require 'erb' template =< <%= variable %>

Slide 84

Slide 84 text

10

Slide 85

Slide 85 text

# erb.rb:856 def result(b=new_toplevel) if @safe_level proc { $SAFE = @safe_level eval(@src, b, (@filename || '(erb)'), @lineno) }.call else eval(@src, b, (@filename || '(erb)'), @lineno) end end Source:

Slide 86

Slide 86 text

No content

Slide 87

Slide 87 text

class Cat def to_s a = 1 b = 2 puts "CAT" end end

Slide 88

Slide 88 text

bindings = [] trace = TracePoint.new(:return) do |tp| puts tp.path puts tp.lineno bindings << tp.binding end trace.enable c = Cat.new c.to_s trace.disable c = nil

Slide 89

Slide 89 text

p bindings.first.local_variables # [:a, :b] p bindings.first.local_variable_get(:a) # 1 p bindings.first.local_variable_get(:b) # 2 p bindings.first.receiver # #

Slide 90

Slide 90 text

require 'pry' class Cat def traced_m a = 1 b = 2 puts "CAT" end end

Slide 91

Slide 91 text

trace = TracePoint.new(:return) do |tp| tp.binding.pry if tp.method_id == :traced_m end trace.enable c = Cat.new c.traced_m

Slide 92

Slide 92 text

No content

Slide 93

Slide 93 text

We're going to write microcontainer for variables – OpenStruct style (because we can)

Slide 94

Slide 94 text

Common for all examples class Binder def initialize(b) @binding = b end end

Slide 95

Slide 95 text

V1

Slide 96

Slide 96 text

def method_missing(name, *args) if @binding.local_variables.include?(name) @binding.local_variable_get(name) elsif name.to_s[-1] == '=' @binding.local_variable_set(name.to_s.del ete('=').to_sym, args[0]) else super end end

Slide 97

Slide 97 text

How we're going to benchmark it?

Slide 98

Slide 98 text

class GetBinding def self.get_binding a = 10 str = 'a1b2' binding end end

Slide 99

Slide 99 text

b = GetBinding.get_binding hash = { a: 10, str: 'a1b2' } puts 'creation' Benchmark.ips do |x| x.report { namespace::Binder.new(b) } x.report { OpenStruct.new(hash) } x.compare! end

Slide 100

Slide 100 text

binder = namespace::Binder.new(b) struct = OpenStruct.new(hash) puts 'get' Benchmark.ips do |x| x.report { binder.a; binder.str } x.report { struct.a; struct.str } x.compare! end

Slide 101

Slide 101 text

puts 'set' Benchmark.ips do |x| x.report { binder.a = 2; binder.string_2 = 'abc' } x.report { struct.a = 2; struct.string_2 = 'abc' } x.compare! end puts 'set different' Benchmark.ips do |x| x.report { binder.send("var_#{n}=".to_sym, 1) } x.report { struct.send("var_#{n}=".to_sym, 1) } x.compare! end

Slide 102

Slide 102 text

Version V1 creation V1 binder: 2849210.5 i/s OpenStruct: 719580.0 i/s - 3.96x slower get OpenStruct: 3243263.4 i/s V1 binder: 502957.6 i/s - 6.45x slower set OpenStruct: 1390626.0 i/s V1 binder: 172057.9 i/s - 8.08x slower set different OpenStruct: 1023580.8 i/s V1 binder: 265165.3 i/s - 3.86x slower

Slide 103

Slide 103 text

V2

Slide 104

Slide 104 text

def initialize(b) @binding = b @binding.local_variables.each do |var| l = -> { @binding.local_variable_get(var) } define_singleton_method(var, l) end end

Slide 105

Slide 105 text

Version V2 creation OpenStruct: 691431.3 i/s V2 binder: 135494.8 i/s - 5.10x slower get OpenStruct: 3242890.9 i/s V2 binder: 2768257.5 i/s - same-ish: difference falls within error set OpenStruct: 1708887.9 i/s V2 binder: 204508.0 i/s - 8.36x slower set different OpenStruct: 1173427.4 i/s V2 binder: 298250.5 i/s - 3.93x slower

Slide 106

Slide 106 text

V3

Slide 107

Slide 107 text

def initialize(b) @binding = b @binding.local_variables.each do |name| get = -> { @binding.local_variable_get(name) } set = ->(new_var) { @binding.local_variable_set(name, new_var) } define_singleton_method(name, get) define_singleton_method("#{name}=".to_sym, set) end end

Slide 108

Slide 108 text

Version V3 creation OpenStruct: 724209.0 i/s V3 binder: 96906.5 i/s - 7.47x slower get OpenStruct: 3298616.7 i/s V3 binder: 2970314.4 i/s - same-ish: difference falls within error set OpenStruct: 1609587.6 i/s V3 binder: 398771.9 i/s - 4.04x slower set different OpenStruct: 985401.4 i/s V3 binder: 273593.3 i/s - 3.60x slower

Slide 109

Slide 109 text

V4

Slide 110

Slide 110 text

def method_missing(name, *args) if @binding.local_variables.include?(name) @binding.local_variable_get(name) elsif name.to_s[-1] == '=' pure_name = name.to_s.delete('=').to_sym @binding.local_variable_set(pure_name, args[0]) unless methods.include?(pure_name) set = ->(new_var) { @binding.local_variable_set(pure_name, new_var) } get = -> { @binding.local_variable_get(pure_name) } define_singleton_method(name, set) define_singleton_method(pure_name, get) end else super end end

Slide 111

Slide 111 text

Version V4 creation OpenStruct: 733813.6 i/s V4 binder: 82442.4 i/s - 8.90x slower get OpenStruct: 3615367.7 i/s V4 binder: 2928936.0 i/s - same-ish difference falls within error set V4 binder: 2637039.0 i/s OpenStruct: 1779210.5 i/s - 1.48x slower set different V4 binder: 1295138.7 i/s OpenStruct: 1172012.4 i/s - same-ish difference falls within error

Slide 112

Slide 112 text

https://github.com/esse/openstruct_in_binding

Slide 113

Slide 113 text

https://github.com/esse/bb_openstruct gem 'bb_openstruct'

Slide 114

Slide 114 text

No content

Slide 115

Slide 115 text

creation Openstruct: 719494.0 i/s BBOpenstruct: 54072.0 i/s - 13.31x slower get Openstruct: 3548217.5 i/s BBOpenstruct: 3170859.8 i/s - same-ish: difference falls within error set BBOpenstruct: 2470835.7 i/s Openstruct: 1730367.3 i/s - 1.43x slower set different Openstruct: 1163716.8 i/s BBOpenstruct: 1145259.5 i/s - same-ish: difference falls within error

Slide 116

Slide 116 text

… and (almost) full openstruct compatibility!

Slide 117

Slide 117 text

Fun fact

Slide 118

Slide 118 text

Proc and lambda are actually same struct in C (only difference is one flag)

Slide 119

Slide 119 text

Questions?

Slide 120

Slide 120 text

Thanks! Proudly illustrated by Emilia Mucha [email protected]