This document summarizes Rubinius, an implementation of the Ruby programming language that includes a bytecode virtual machine written in C++ and Ruby. Some key points:
- Rubinius compiles Ruby code to bytecode that runs on its built-in virtual machine. This provides performance improvements over interpreting Ruby code.
- The virtual machine is implemented in both C++ and Ruby to provide flexibility. It can inline methods, perform just-in-time compilation, and garbage collect memory.
- Rubinius aims to be a complete Ruby implementation while also improving performance through techniques like inline caching, profiling, and garbage collection optimizations.
21. 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 23
i = 0
0012: push_local 0 # i
while i < 1000 do 0014: meta_push_1
i = i + 1 0015: meta_send_op_plus :+
0017: set_local 0 # i
end 0019: pop
0020: check_interrupts
0021: goto 4
0023: push_nil
0024: pop
0025: push_true
0026: ret
22. Fancy
class 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}"
}
}
29. 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
self
end
33. 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"
end
end
44. module Named
def name
class Person
"named"
def name
end
"me"
end
end
class Person
end
include Named
end
45. module Named
def name class Person
class Person
"named" end
def name
end
"me"
end def p.name
end
class Person "specific"
end
include Named end
end
46. class Person
attr_accessor :name
end
10000.times do
p = Person.new
p.name
end
47. There are only two hard
problems in Computer Science:
cache invalidation,
naming things
and off-by-one errors
48.
49. module Naming
def name
"me2"
end
end
class Person
include Naming
end
10000.times do
p = Person.new
p.name
end
50. module Naming
def name
"me2" class Person
end def name
end "new_name"
end
class Person end
include Naming
end 10000.times do
p = Person.new
10000.times do p.name
p = Person.new end
p.name
end
53. “...we finally managed to get our Linux (...) builds to use
GCC 4.5, ... and profile guided optimization enabled”
Mike Hommey - on Firefox 6 performance
54. def method1
1 + 1
end
def method2
2 + 1
end
10000.times do
method1
end
65. array = [1] * 1000000
array.each do |element|
puts "element: #{element}"
end
66.
67. def method1
1 + 1
end
def method2
method1
end
100.times do
method2
end
68. def method1 def method1
1 + 1 1 + 1
end end
def method2 def method2
method1 method1
end end
100.times do 100.times do
method2 method1
end end
69. def method1 def method1
1 + 1 1 + 1
end end
def method2 def method2
method1 method1
end end
100.times do 100.times do
method2 method1
end end
70. def method1 def method1 def method1
1 + 1 1 + 1 1 + 1
end end end
def method2 def method2 def method2
method1 method1 method1
end end end
100.times do 100.times do 100.times do
method2 method1 1 + 1
end end end
71. def method1 def method1 def method1
1 + 1 1 + 1 1 + 1
end end end
def method2 def method2 def method2
method1 method1 method1
end end end
100.times do 100.times do 100.times do
method2 method1 1 + 1
end end end
72.
73. def awesome(x)
return 0 if x == 0
x + 1
end
def use_awesomeness_regular
a = awesome(0)
b = awesome(1)
a + b
end
74. 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 + b
end
75.
76. array = [1] * 1000000
array.each do |element|
puts "element: #{element}"
end
77. array = [1] * 1000000
array = [1] * 1000000
i = 0
size = array.size
array.each do |element|
while i < size
puts "element: #{element}"
puts "element: #{array[i]}"
end
i += 1
end
78. array = [1] * 1000000
array = [1] * 1000000
i = 0
array.each do |element|
puts "element: #{element}"
== size = array.size
while i < size
puts "element: #{array[i]}"
end
i += 1
end
79.
80. array = [1] * 100
array.each do |element|
puts "element: #{element}"
b = 2
end
puts b
81. array = [1] * 100
array = [1] * 100
i = 0
size = array.size
array.each do |element|
while i < size
puts "element: #{element}"
puts "element: #{array[i]}"
b = 2
b = 2
end
i += 1
end
puts b
puts b
82. array = [1] * 100
array = [1] * 100
i = 0
size = array.size
array.each do |element|
while i < size
puts "element: #{element}"
b = 2 != puts "element: #{array[i]}"
b = 2
end
i += 1
end
puts b
puts b
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&#x2019;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&#x2019;t specify types when writing ruby code, but during runtime it&#x2019;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&#x2019;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&#x2019;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&#x2019;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&#x2019;t mean other languages can&#x2019;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&#x2019;s a bunch of other languages people created, mostly as an experiment and not very mature\n
There&#x2019;s a bunch of other languages people created, mostly as an experiment and not very mature\n
There&#x2019;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&#x2019;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&#x2019;s easier for people to help out with. You don&#x2019;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&#x2019;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&#x2019;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&#x2019;s only available for this specific instance of the Person.\n
So where can this method be?\n- There&#x2019;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&#x2019;s only available for this specific instance of the Person.\n
So where can this method be?\n- There&#x2019;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&#x2019;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&#x2019;s static anyway. \n\nSo the expensive computation can perhaps be stored and reused so it&#x2019;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&#x2019;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&#x2019;s isn&#x2019;t a lot of memory overhead.\n\n\nSo we don&#x2019;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&#x2019;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 &#x201C;name&#x201D;.\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&#x2019;t know ahead of time that we don&#x2019;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&#x2019;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&#x2019;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&#x2019;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&#x2019;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&#x2019;t inlined. These properties can be determined by analyzing the bytecode for a method. If these issues come forth from it, the code isn&#x2019;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&#x2019;t inlined. These properties can be determined by analyzing the bytecode for a method. If these issues come forth from it, the code isn&#x2019;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&#x2019;t inlined. These properties can be determined by analyzing the bytecode for a method. If these issues come forth from it, the code isn&#x2019;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&#x2019;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&#x2019;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&#x2019;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&#x2019;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&#x2019;t want to copy them, since copying is an expensive operation and they also don&#x2019;t fit in the immix blocks. \n\nThis special area doesn&#x2019;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&#x2019;s an instance variable table, because you can add and remove instance variables on the fly in Ruby. There&#x2019;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&#x2019;s interesting and want to contribute, just one patch accepted means commit access. \n