• Share
  • Email
  • Embed
  • Like
  • Save
  • Private Content
Lecture on Rubinius for Compiler Construction at University of Twente
 

Lecture on Rubinius for Compiler Construction at University of Twente

on

  • 1,384 views

Slides for the guest lecture at the Compiler Construction (Vertalerbouw) course at the University of Twente.

Slides for the guest lecture at the Compiler Construction (Vertalerbouw) course at the University of Twente.

Statistics

Views

Total Views
1,384
Views on SlideShare
1,382
Embed Views
2

Actions

Likes
0
Downloads
7
Comments
0

1 Embed 2

http://www.linkedin.com 2

Accessibility

Categories

Upload Details

Uploaded via as Apple Keynote

Usage Rights

© All Rights Reserved

Report content

Flagged as inappropriate Flag as inappropriate
Flag as inappropriate

Select your reason for flagging this presentation as inappropriate.

Cancel
  • Full Name Full Name Comment goes here.
    Are you sure you want to
    Your message goes here
    Processing…
Post Comment
Edit your comment
  • \n
  • \n
  • \n
  • Regular contributor since the early beginnings of 2008\n
  • First I want to explain two concepts\nThe first concept is about dynamic versus static languages.\n\n- Dynamic languages\nTypes checking is done at runtime\n- Static languages\nTypes are checked during compile time\n\n \n
  • The first is what happens in a dynamic language. Types are not fixed and you can use different types if you want\nThe second is a static language. Once a certain type is defined, the type can’t be changed. \n
  • Another difference is how types are enforced.\nThis is often confused with static and dynamic languages, seeing the dynamic language as weakly typed. This however is often not true.\n\n-Strong typing\nThis enforces specific rules and behavior on what happens when you run certain operations on differently typed objects\n-Weak typing\nImplicit type conversion when different types are used\n\n
  • \n
  • So what is Ruby?\n\nRuby is a dynamically strongly typed language. So you don’t specify types when writing ruby code, but during runtime it’s types are not just changed automatically.\n\nIt has primarily been influenced by Perl, Smalltalk and Lisp. The primary design philosophy is that a language should be productive and fun to use. \n
  • So how does it look?\nThe classic Hello world! example\n
  • This is how basic class definition and method definition look like\n
  • Block syntax. They are lambda like constructs. It’s like passing a piece of code as a special argument to a method. \n
  • Modules. They allow for code being mixed into other classes. It’s a bit like multiple inheritance. This is made possible because of the dynamic nature of Ruby\n
  • Rubinius includes everything needed to run Ruby code\n
  • That means it includes a bytecode Virtual Machine, primarily designed for running Ruby code.\n
  • But it also includes all core classes etc. you expect in Ruby, like String, Hash and Array. In other languages t’s often called the standard library, but that has a different meaning in Ruby, so hence this name. \n
  • It was started in 2006, thought up by Evan Phoenix during his honeymoon. He and Brian Ford are payed full time to work on Rubinius.\n
  • So what does the virtual machine entail? \n
  • The first and initial version was written in Ruby. After this proof of concept, a first version of the virtual machine was written C. In 2008 the choice was made to rewrite it in C++, because that is a better fit with the architecture of the VM. \n
  • The bytecode instruction set includes the necessary instructions for running Ruby code efficiently. This doesn’t mean other languages can’t be written on top of Rubinius.\n
  • So what does the bytecode look like?\nThe comments at the end show how the variable names are mapped to slots\n
  • One example is Fancy. It has a few different aspect compare to ruby, like named parameters\n
  • There’s a bunch of other languages people created, mostly as an experiment and not very mature\n
  • There’s a bunch of other languages people created, mostly as an experiment and not very mature\n
  • There’s a bunch of other languages people created, mostly as an experiment and not very mature\n
  • So how do we make it fast? There’s quite a few techniques for that which will be discussed later in the lecture\n
  • The kernel code is written in Ruby as much as possible. This means that classes like String, Hash and Array are written in Ruby itself\n
  • This is how Array#each looks like. It uses a Tuple, which is a fixed size array like structure that Array is built upon. Hash is written in Ruby too\n
  • Having more in Ruby also means that it’s easier for people to help out with. You don’t need to know C / C++ in order to contribute to Rubinius\n
  • \n
  • \n
  • It parses into an AST and then outputs bytecode by walking the AST. Each node knows how to emit bytecode, such as this example for Fixnum literals. It stores the line number and the given value for the literal.\n
  • People often claim that Ruby, or dynamic languages in general are slow. \n
  • \n
  • Add a method useful for you on a core type. Not always the best solution, but it’s a very flexible way to handle things. \n
  • You can also do very nasty things.\n
  • \n
  • \n
  • So in order to improve performance of a dynamic language, there are various techniques. One of the most basic ones that gives an easy improvement is inline caching.\n
  • So we have this little piece of code. First, we create a Person, then we do some other stuff and then we call the method name on it. p.name here is know as a call site.\n
  • So where can this method be?\n- There’s the simple case of it being a method defined for all instances of the class\n-It can also be in a module included into the class\n-Another option is defining a method on the metaclass of the object. This means it’s only available for this specific instance of the Person.\n
  • So where can this method be?\n- There’s the simple case of it being a method defined for all instances of the class\n-It can also be in a module included into the class\n-Another option is defining a method on the metaclass of the object. This means it’s only available for this specific instance of the Person.\n
  • So where can this method be?\n- There’s the simple case of it being a method defined for all instances of the class\n-It can also be in a module included into the class\n-Another option is defining a method on the metaclass of the object. This means it’s only available for this specific instance of the Person.\n
  • So consider this extended example with complete code. If you look at the code, you see that when running this, p.name ends up in the same method every time you execute the code. So even though Ruby is very dynamic, most of the code looks like it’s static anyway. \n\nSo the expensive computation can perhaps be stored and reused so it’s not needed each time. The caching of this method dispatch result is called inline caching.\n
  • \n
  • So, here we need to be sure to invalidate our cache, otherwise it would run the wrong method\n
  • So, here we need to be sure to invalidate our cache, otherwise it would run the wrong method\n
  • So, now the question is, is this lookup changed too? The problem is that we want to keep the caches simple. So there’s a cache entry at each call site that stores for a specific type, the method it dispatched to and module that method is defined. This makes the cache entries small so there’s isn’t a lot of memory overhead.\n\n\nSo we don’t want to store the complete chain. Which means that we only have OtherObject as a type stored here. It also means that we need to invalidate the cache because we don’t know whether OtherObject includes Naming or not. \n\nTherefore invalidating caches is a brute force measure that removes all caches with the same name, so in this case “name”.\n
  • Just In Time compilation means that code is compiled to native code during execution of your programs. \n
  • This quote shows that using runtime information, you can optimize code a lot better than just only ahead of time. \n\nSo we need to track this runtime information so we know what we can compile into native code. \n
  • So here we have a bunch of code that is executed quite often. We don’t know ahead of time that we don’t actually need the method2 method, but we do during runtime. So we can use this information at runtime.\n
  • Each VMMethod object keeps track of various things\nYou can see how often a method is called\nThe llvm_function_ pointer points at jitted code for this method\nThere’s a name for the method of course\nAnd a whole bunch of other stuff removed here\n
  • So right now, a method is going to be compiled after it has been executed 4000 times. \n
  • The initial version used hand crafted assembly to output the code for it. This was cumbersome and would need a lot of work to optimize it to a decently performing level. \n
  • That’s why the LLVM compiler infrastructure was chosen. It already did a huge amount of legwork in generating good native code for various platforms. \n
  • \n
  • This is what the LLVM bytecode looks like without any optimizations. This is actually kind of like how with Rubinius code is translated. There is a C++ API for creating this bytecode. \n
  • This is what the LLVM optimizer makes out of it. It can reason about so in the end the value 10 can be returned directly. LLVM can run similar passes over code generated through the C++ API. \n
  • So how is this implemented? The code compilation actually happens in a background thread. The virtual machine requests that a certain method is jitted, which is then given to the LLVM thread. The LLVM thread then goes to work and creates the LLVM bytecode. After this is compiled, it sets the llvm_function_ pointer seen earlier. \n\nThis means that the VM just runs along nicely without being interrupted by the compilation of code. \n
  • \n
  • No overhead is faster than no overhead. We already saw that inline caches can greatly improve method dispatching, but there’s certainly room for even more optimizations. One of these is code inlining, which means that method dispatch overhead is completely removed. \n\n\n
  • Ruby has another place that can benefit greatly from code inline, which is the usage of blocks. A block is a construct which has it’s own scope and can also be captured explicitly. This means that it does add overhead and looking at removing that overhead is very interesting. \n
  • So we start here with a simple piece of code. What inlining does, is moving the actual code of the method into the method that the method is called from. So this means that instead of dispatching a method, it executes the code of that method directly. \n
  • So we start here with a simple piece of code. What inlining does, is moving the actual code of the method into the method that the method is called from. So this means that instead of dispatching a method, it executes the code of that method directly. \n
  • So we start here with a simple piece of code. What inlining does, is moving the actual code of the method into the method that the method is called from. So this means that instead of dispatching a method, it executes the code of that method directly. \n
  • So we start here with a simple piece of code. What inlining does, is moving the actual code of the method into the method that the method is called from. So this means that instead of dispatching a method, it executes the code of that method directly. \n
  • So we start here with a simple piece of code. What inlining does, is moving the actual code of the method into the method that the method is called from. So this means that instead of dispatching a method, it executes the code of that method directly. \n
  • So inline has some great potential, but there are quite a few caveats. \n
  • We take a look at this example. Here is the same method called with a different parameter, which we could perhaps inline. So we take a naive attempt in the next slide.\n
  • Here we manually inlined the two calls to awesome() and injected the code in this place. The begin / end block is used so we have the correct scope of the inlined code.\n\nBut if we look at this, this code behaves differently! Let me show this by running it. What you can see here is that the control flow is different because of the return. While the return first meant that it would return from the method, with the inlining this method is no longer present. So when inlining, control flow is something that needs considering. \n
  • This is a very simple example of how block code inlining should work. The left version is a much prettier and nicer version and that is how Ruby code should look.\n\nThe example on the right is equivalent, but it has the block used in the version on the left removed. This is what you can call block inlining in Rubinius. \n
  • This is a very simple example of how block code inlining should work. The left version is a much prettier and nicer version and that is how Ruby code should look.\n\nThe example on the right is equivalent, but it has the block used in the version on the left removed. This is what you can call block inlining in Rubinius. \n
  • This is a very simple example of how block code inlining should work. The left version is a much prettier and nicer version and that is how Ruby code should look.\n\nThe example on the right is equivalent, but it has the block used in the version on the left removed. This is what you can call block inlining in Rubinius. \n
  • But beware with scoping issues. Who thinks he knows what happens here?\n
  • But beware with scoping issues. Who thinks he knows what happens here?\n
  • But beware with scoping issues. Who thinks he knows what happens here?\n
  • So there are few reasons why code isn’t inlined. These properties can be determined by analyzing the bytecode for a method. If these issues come forth from it, the code isn’t inlined. What can be inlined is something that is improved over time. More code can be written to support more complex structures for inlining. \n
  • So there are few reasons why code isn’t inlined. These properties can be determined by analyzing the bytecode for a method. If these issues come forth from it, the code isn’t inlined. What can be inlined is something that is improved over time. More code can be written to support more complex structures for inlining. \n
  • So there are few reasons why code isn’t inlined. These properties can be determined by analyzing the bytecode for a method. If these issues come forth from it, the code isn’t inlined. What can be inlined is something that is improved over time. More code can be written to support more complex structures for inlining. \n
  • \n
  • People are really happy that others clean up their garbage. No need to worry about it anymore. Using automatic memory management has various advantages, such as not having to worry about object ownership, double free bugs and possible memory leaks. \n\nRuby also uses automatic memory management and needs garbage collection.\n
  • A simple naive way of doing garbage collection is to start at the root of the object graph and go from there. Going through each object, marking the objects as you go along. After this marking phase, you go through all objects again. Everything that doesn’t have a mark set, is freed as it is apparently not reachable anymore. \n
  • So how can we make this faster? We can look at the properties of different objects. Young objects are often only around for a very short time. So we want to have a mechanism to suit this properly. \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • Immix uses different block of memory. It allocates objects in a block, so it has contiguous allocation for objects. This means it doesn’t need a free list for allocation. On the other hand, this technique can limit memory fragmentation.\n
  • Immix uses different block of memory. It allocates objects in a block, so it has contiguous allocation for objects. This means it doesn’t need a free list for allocation. On the other hand, this technique can limit memory fragmentation.\n
  • Immix uses different block of memory. It allocates objects in a block, so it has contiguous allocation for objects. This means it doesn’t need a free list for allocation. On the other hand, this technique can limit memory fragmentation.\n
  • Very large objects are directly allocated in a special area. This is because you don’t want to copy them, since copying is an expensive operation and they also don’t fit in the immix blocks. \n\nThis special area doesn’t copy objects, but just uses a very simple mark and sweep algorithm.\n
  • \n
  • This is kind of how an object looks like in memory. By default there’s an instance variable table, because you can add and remove instance variables on the fly in Ruby. There’s no definitive way to know which instance variable we need up front.\n\nBut we can make a very nice educated guess on how it would look like. We use the compiler to track all the variables we see. So if we compile the class given here, we can track all instance variables we encounter. \n
  • Here we see the effect on memory usage. With the instance variable table, it uses 160 bytes for each object of type Address, but with the packing it only uses 56 bytes!\n
  • \n
  • So if you want to know more, please look it up here. You can also of course ask me additional questions. \n
  • If you think it’s interesting and want to contribute, just one patch accepted means commit access. \n
  • \n

Lecture on Rubinius for Compiler Construction at University of Twente Lecture on Rubinius for Compiler Construction at University of Twente Presentation Transcript

  • Rubinius Use Ruby
  • Dirkjan Bussink d.bussink@gmail.com
  • 2008
  • Dynamic vs. Static
  • x = 10 x = aint x = 10string x = a => ERROR
  • Strong vs.Weak
  • x = 10y = "20"x + y# Javascript=> 1020# PHP=> 30
  • Ruby
  • puts "Hello world!"
  • class MyAwesomeObject def initialize puts "Running constructor" end def cool_method(arg1, arg2) arg1 + arg2 endend
  • [1, 2, 3, 4].each do |e| puts eend
  • module Person def name puts "Every person has name" endendclass Student include PersonendStudent.new.name
  • Rubinius
  • Virtual Machine
  • Kernel
  • 2006Evan Phoenix Brian Ford
  • Virtual Machine
  • C++
  • Ruby
  • 0000: meta_push_0 0001: set_local 0 # i 0003: pop 0004: push_local 0 # i 0006: push_literal 1000 0008: meta_send_op_lt :< 0010: goto_if_false 23i = 0 0012: push_local 0 # iwhile i < 1000 do 0014: meta_push_1 i = i + 1 0015: meta_send_op_plus :+ 0017: set_local 0 # iend 0019: pop 0020: check_interrupts 0021: goto 4 0023: push_nil 0024: pop 0025: push_true 0026: ret
  • Fancyclass Person { read_write_slots: [name, age, city] def initialize: @name age: @age city: @city { } def go_to: city { if: (city is_a?: City) then: { @city = city } } def to_s { "Person: #{@name}, #{@age} years old, living in #{@city}" }}
  • JavaScript
  • JavaScriptPython
  • JavaScriptPythonBrainfuck
  • Ruby in Ruby
  • def each return to_enum(:each) unless block_given? i = @start total = i + @total tuple = @tuple while i < total yield tuple.at(i) i += 1 end selfend
  • Ruby for Rubyists
  • Improveeverything
  • Compiler
  • class FixnumLiteral < NumberLiteral def initialize(line, value) @line = line @value = value end def bytecode(g) pos(g) g.push @value end def defined(g) g.push_literal "expression" endend
  • Ruby is slow!
  • Flexibility
  • class Array def sum inject(0) {|total, e| total + e.to_i} endend
  • class Bignum def +(other) self - other endend
  • "The edges of the sword are life and death, no one knows which is which" Ikkyu Sojun, 15th Century Zen master
  • Hard, but not impossible
  • Inline caching
  • p = Person.new...p.name
  • class Person def name "me" endend
  • module Named def nameclass Person "named" def name end "me" end end class Personend include Named end
  • module Named def name class Personclass Person "named" end def name end "me" end def p.name end class Person "specific"end include Named end end
  • class Person attr_accessor :nameend10000.times do p = Person.new p.nameend
  • There are only two hardproblems in Computer Science: cache invalidation, naming things and off-by-one errors
  • module Naming def name "me2" endendclass Person include Namingend10000.times do p = Person.new p.nameend
  • module Naming def name "me2" class Person end def nameend "new_name" endclass Person end include Namingend 10000.times do p = Person.new10000.times do p.name p = Person.new end p.nameend
  • p = OtherObject.new...p.name
  • JIT
  • “...we finally managed to get our Linux (...) builds to use GCC 4.5, ... and profile guided optimization enabled” Mike Hommey - on Firefox 6 performance
  • def method1 1 + 1enddef method2 2 + 1end10000.times do method1end
  • members of rubinius::VMMethod:total_args = 0,call_count = 21,llvm_function_ = 0x0,name_ = 0x6306,
  • static const int default_jit_call_til_compile = 4000;
  • pushl %ebpmovl %esp, %ebpsubl $4, %espmovl $10, -4(%ebp)leal -4(%ebp), %eaxaddl $66, (%eax)leaveret
  • #include <stdio.h>int func() { int i = 0; i += 10; return i;}
  • ; ModuleID = <stdin>target datalayout = "e-p:64:64:64-i1:8:8-i8:8:8-i16:16:16-i32:32:32-i64:64:64-f32:32:32-f64:64:64-v64:64:64-v128:128:128-a0:0:64-s0:64:64-f80:128:128-n8:16:32:64"target triple = "x86_64-apple-darwin10.7"define i32 @func() nounwind ssp {entry: %retval = alloca i32 %0 = alloca i32 %i = alloca i32 %"alloca point" = bitcast i32 0 to i32 store i32 0, i32* %i, align 4 %1 = load i32* %i, align 4 %2 = add nsw i32 %1, 10 store i32 %2, i32* %i, align 4 %3 = load i32* %i, align 4 store i32 %3, i32* %0, align 4 %4 = load i32* %0, align 4 store i32 %4, i32* %retval, align 4 br label %returnreturn: ; preds =%entry %retval1 = load i32* %retval ret i32 %retval1}
  • ; ModuleID = <stdin>target datalayout = "e-p:64:64:64-i1:8:8-i8:8:8-i16:16:16-i32:32:32-i64:64:64-f32:32:32-f64:64:64-v64:64:64-v128:128:128-a0:0:64-s0:64:64-f80:128:128-n8:16:32:64"target triple = "x86_64-apple-darwin10.7"define i32 @func() nounwind readnone ssp {entry: ret i32 10}
  • Go JIT! RBX LLVMthread(s) thread Here it is!
  • members of rubinius::VMMethod:total_args = 0,call_count = 21,llvm_function_ = 0x102c07b30,name_ = 0x6306,
  • Code inlining
  • array = [1] * 1000000array.each do |element| puts "element: #{element}"end
  • def method1 1 + 1enddef method2 method1end100.times do method2end
  • def method1 def method1 1 + 1 1 + 1end enddef method2 def method2 method1 method1end end100.times do 100.times do method2 method1end end
  • def method1 def method1 1 + 1 1 + 1end enddef method2 def method2 method1 method1end end100.times do 100.times do method2 method1end end
  • def method1 def method1 def method1 1 + 1 1 + 1 1 + 1end end enddef method2 def method2 def method2 method1 method1 method1end end end100.times do 100.times do 100.times do method2 method1 1 + 1end end end
  • def method1 def method1 def method1 1 + 1 1 + 1 1 + 1end end enddef method2 def method2 def method2 method1 method1 method1end end end100.times do 100.times do 100.times do method2 method1 1 + 1end end end
  • def awesome(x) return 0 if x == 0 x + 1enddef use_awesomeness_regular a = awesome(0) b = awesome(1) a + bend
  • def use_awesomeness_inlined a = begin return 0 if 0 == 0 0 + 1 end b = begin return 1 if 1 == 0 1 + 1 end a + bend
  • array = [1] * 1000000array.each do |element| puts "element: #{element}"end
  • array = [1] * 1000000array = [1] * 1000000 i = 0 size = array.sizearray.each do |element| while i < size puts "element: #{element}" puts "element: #{array[i]}"end i += 1 end
  • array = [1] * 1000000array = [1] * 1000000 i = 0array.each do |element| puts "element: #{element}" == size = array.size while i < size puts "element: #{array[i]}"end i += 1 end
  • array = [1] * 100array.each do |element| puts "element: #{element}" b = 2endputs b
  • array = [1] * 100array = [1] * 100 i = 0 size = array.sizearray.each do |element| while i < size puts "element: #{element}" puts "element: #{array[i]}" b = 2 b = 2end i += 1 endputs b puts b
  • array = [1] * 100array = [1] * 100 i = 0 size = array.sizearray.each do |element| while i < size puts "element: #{element}" b = 2 != puts "element: #{array[i]}" b = 2end i += 1 endputs b puts b
  • Control flow issues
  • Control flow issuesScoping issues
  • Control flow issuesScoping issuesToo big piece of code
  • GarbageCollection
  • MarkSweep
  • Generational Garbage Collection
  • YoungMatureLarge
  • YoungSemi Space collector
  • Mature Immix
  • LargeMark - sweep
  • Creating less garbage
  • class Address attr_reader :street attr_reader :number attr_reader :cityend
  • class Address attr_reader :street attr_reader :number attr_reader :city endAddress.instance_variable_get("@seen_ivars")=> [:@street, :@number, :@city]
  • a = Address.new a.street = "Street" a.number = "1" a.city = "Enschede"Rubinius.memory_size(a) => 56 VSRubinius.memory_size(a) => 160
  • What else?
  • http://rubini.us/https://github.com/evanphx/rubinius
  • 1 patch == commit access
  • Questions?