Yes, yes, finally a Ruby VM that fits in your backpocket, it’s there, it’s here. I mean, yay!

It all started 2 weeks ago. I was stocked by Potion and had been studying Lua and other VMs for a little while. So I dove in and hacked a little Ruby VM with bytecode and all.

It's Tiny Because There's Not A Lot in There

The primary goal is to keep it small. This way, lots of people can understand it and change it quickly. My objective is to keep the VM (the C part) under 64K. Right now, it’s 43K and 1541 LOC. Not sure it’s possible, but I’ll take this as a challenge.

But, some very basic things are missing: no Float, no Module, no Proc or Bloc, no Array, Hash, IO, basically, almost nothing. But most of the keywords and core objects are there: Class, Object, Fixnum, Symbol, String, def, class, if, unless, while, until, etc.

The lexer is written in Ragel and the parser in Lemon. I wish to support the commonly used stuff in Ruby but give up on the dark corners or things that are too complex. Right now the grammar is just 100 LOC, that’s nice. The rule is:

Everything in tinyrb should run in the big Ruby. (except bugs and things that don’t comply to the principle of least surprise.) But not everything in the big Ruby should run in tinyrb.

It's Kinda Fast, For Now At Least

I implemented a couple optimizations already, Monomorphic method cache is there, means method lookup is cached at the call site. Also, as a test, I inline 3 methods, Fixnum#+, Fixnum#-, Fixnum#<, which makes a pretty big difference. The interpreter loop uses direct threaded dispatch when available and falls back to while-switch for portability.

Of course, I ran a couple micro-benchmarks. Now you know what they say about benchmarks? So take those with a grain of salt. But I think it just shows that a “fast” interpreter doesn’t need to be huge, at least…

UPDATE well of course I did something wrong here, I forgot to run jruby w/ the -server option. So please consider this benchmark completely false and useless.

Classic fibonaci

MRI:      9.659s
tinyrb:   7.865s
JRuby:    6.755s
Rubinius: 6.006s
YARV:     2.393s

bm_vm2_method.rb

MRI:      17.136s
tinyrb:   12.155s
JRuby:    11.135s
Rubinius: 12.580s
YARV:     7.165s

memory usage for bm_vm2_method.rb

 PID COMMAND      %CPU   TIME   #TH #PRTS #MREGS RPRVT  RSHRD  RSIZE  VSIZE
3041 tinyrb      99.5%  0:03.54   1    13     25  412K   184K   940K    18M 
3045 java        99.8%  0:04.15  10   228    190   28M+  216K-   28M   705M 
3058 ruby19      99.8%  0:03.84   2    30     40  960K   184K  2452K    20M 

tinyrb is far from the fastest but it’s -still very close to JRuby and Rubinius- faster then MRI here and hey! it’s just 2 weeks old ok?

Hacking tinyrb 101

To compile it:

git clone git://github.com/macournoyer/tinyrb.git
cd tinyrb
make
make test # optional

This should produce a nice tinyrb file.

You can use tinyrb like you usually do with ruby, see the -h option for usage..

./tinyrb -e "puts 1"
./tinyrb test/loop.rb

You can see the generated bytecode using the -d option and you’ll get something like that:

; block definition: 0x6cf78 (level 0)
; 2 registers ; 1 nested blocks
; 0 args
.value  fib      ; 0
.value  34       ; 1
[000] def          0   0   0 ; fib => 0x6cf20
[001] self         0   0   0
[002] loadk        2   0   1 ; R[2] = 34
[003] boing        0   0   0
[004] lookup       0   0   0 ; R[1] = R[0].method(:fib)
[005] call         0   1   0 ; R[0] = R[0].R[1](1)
[006] return       0   0   0
; block end

I’ll talk more about the bytecode later.

Lets Talk About It At The Next Montreal.rb

If you wanna talk about tinyrb, come see my next talk about Ruby VMs at the next Montreal.rb.

Or join me on github, or #tinyrb on freenode.