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.