• Stars
    star
    418
  • Rank 103,620 (Top 3 %)
  • Language
    Ruby
  • License
    Apache License 2.0
  • Created about 4 years ago
  • Updated 11 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

JIT for Ruby that is written in Ruby

TenderJIT

TenderJIT is an experimental JIT compiler for Ruby written in Ruby. Its design is mostly based off YJIT.

Getting Started with TenderJIT

TenderJIT isn't available as a gem (yet). To start using it, clone the repository and run the following commands:

$ bundle install
$ bundle exec rake test

If the tests pass, then you're ready to go!

TenderJIT currently requires Ruby 3.0.2 or the edge version of Ruby. It may work on 3.0.X, but I haven't tested older versions.

Running JIT code

Right now, TenderJIT doesn't automatically compile methods. You must manually tell TenderJIT to compile a method.

Let's look at an example:

require "tenderjit"

def fib n
  if n < 3
    1
  else
    fib(n - 1) + fib(n - 2)
  end
end

jit = TenderJIT.new
jit.compile(method(:fib)) # Compile the `fib` method

# Run the `fib` method with the JIT enabled
jit.enable!
fib 8
jit.disable!

Eventually TenderJIT will compile code automatically, but today it doesn't.

TenderJIT only supports Ruby 3.0.2 and up!

How does TenderJIT work?

TenderJIT reads each YARV instruction in the target method, then converts that instruction to machine code.

Let's look at an example of this in action. Say we have a function like this:

def add a, b
  a + b
end

If we disassemble the method using RubyVM::InstructionSequence, we can see the instructions that YARV uses to implement the add method:

$ cat x.rb
def add a, b
  a + b
end

$ ruby --dump=insns x.rb
== disasm: #<ISeq:<main>@x.rb:1 (1,0)-(3,3)> (catch: FALSE)
0000 definemethod                           :add, add                 (   1)[Li]
0003 putobject                              :add
0005 leave

== disasm: #<ISeq:[email protected]:1 (1,0)-(3,3)> (catch: FALSE)
local table (size: 2, argc: 2 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])
[ 2] a@0<Arg>   [ 1] b@1<Arg>
0000 getlocal_WC_0                          a@0                       (   2)[LiCa]
0002 getlocal_WC_0                          b@1
0004 opt_plus                               <calldata!mid:+, argc:1, ARGS_SIMPLE>[CcCr]
0006 leave                                                            (   3)[Re]

The add method calls 4 instructions, 3 of them are unique:

  • getlocal_WC_0
  • opt_plus
  • leave

The YARV virtual machine works by pushing and popping values on a stack. The first two calls to getlocal_WC_0 take one parameter, 0, and 1 respectively. This means "get the local at index 0 and push it on the stack", and "get the local at index 1 and push it on the stack".

After these two instructions have executed, the stack should have two values on it. The opt_plus instructions pops two values from the stack, adds them, then pushes the summed value on the stack. This leaves 1 value on the stack.

Finally the leave instruction pops one value from the stack and returns that value to the calling method.

TenderJIT works by examining each of these instructions, then converts them to machine code at runtime. If a machine code version of the method is available at run-time, then YARV will call the machine code version rather than the YARV byte code version.

Hacking on TenderJIT

You should only need Ruby 3.0.0 or up to get started hacking on TenderJIT. However, I highly recommend installing a debugger like lldb or gdb as well.

The main compiler object is the TenderJIT::ISEQCompiler class which can be found in lib/tenderjit/iseq_compiler.rb.

Each instruction sequence object (method, block, etc) gets its own instance of an ISEQCompiler object.

Each YARV instruction has a corresponding handle_* method in the ISEQCompiler class. The example above used getlocal_WC_0, opt_plus, and leave. Each of these instructions have corresponding handle_getlocal_WC_0, handle_opt_plus, and handle_leave methods in the ISEQCompiler class.

When a request is made to compile an instruction sequence (iseq), the compiler checks to see if there is already an ISEQCompiler object associated with the iseq. If not, it allocates one, then calls compile on the object.

The compiler will compile as many instructions in a row as it can, then will quit compilation. Depending on the instructions that were compiled, it may resume later on.

Not all instructions have corresponding handle_* methods. This just means they are not implemented yet! If you find an instruction you'd like to implement, please do it!

When no corresponding handler function is found, the compiler will generate an "exit" and the machine code will pass control back to YARV. YARV will resume where the compiler left off, so even partially compiled instruction sequences will work.

YARV has a few data structures that you need to be aware of when hacking on TenderJIT. First is the "control frame pointer" or CFP. The CFP represents a stack frame. Each time we call a method, an new stack frame is created.

The CFP points to the iseq it's executing. It also points to the Program Counter, or PC. The PC indicates which instruction is going to execute next. The other crucial thing the CFP points to is the Stack Pointer, or SP. The SP indicates where the top of the stack is, and it points at the "next empty slot" in the stack.

When a function is called, a new CFP is created. The CFP is initialized with the first instruction in the iseq set as the PC, and an empty slot in the SP. When getlocal_WC_0 executes, first it advances the PC to point at the next instruction. Then getlocal_WC_0 fetches the local value, writes it to the empty SP slot, then pushes the SP slot up by one.

TenderJIT gains speed by eliminating PC and SP advancement. This means that as TenderJIT machine code executes, the values on the CFP may not reflect reality! In order to hand control back to YARV, TenderJIT must write accurate values back to the CFP before returning control.

Lazy compilation

TenderJIT is a lazy compiler. It (very poorly) implements a version of Lazy Basic Block Versioning. TenderJIT will only compile one basic block at a time. This means that TenderJIT will stop compiling any time it finds an instruction that might jump somewhere else.

For example:

def add a, b
  puts "hi"

  if a > 0
    b - a
  else
    a + b
  end
end

TenderJIT will compile the method calls as well as the comparison, but when it sees there is a conditional, it will stop compiling. At that point, it inserts a "stub" which is just a way to resume compilation at that point. These "stubs" call back in to the compiler and ask it to resume compilation from that point.

Runtime compilation methods start with compile_* rather than handle_*.

As a practical example, lets look at how the compiler handles the following code:

def get_a_const
  Foo
end

The instructions for this method are as follows:

== disasm: #<ISeq:[email protected]:1 (1,0)-(3,3)> (catch: FALSE)
0000 opt_getinlinecache                     9, <is:0>                 (   2)[LiCa]
0003 putobject                              true
0005 getconstant                            :Foo
0007 opt_setinlinecache                     <is:0>
0009 leave                                                            (   3)[Re]

If we check the implementation of opt_getinlinecache in YARV, we see that it will check a cache. If the cache is valid it will jump to the destination instruction, in this case the instruction at position 9 (you can see that 9 is a parameter on the right of opt_getinlinecache). Since this function can jump, we consider it the end of a basic block. At compile time, TenderJIT doesn't know the machine address where it would have to jump. So it inserts a "stub" which calls the method compile_opt_getinlinecache, but at runtime rather than compile time.

The runtime function will examine the cache. If the cache is valid, it patches the calling jump instruction in the generated machine code to just jump to the destination.

The next time the machine code is run, it no longer calls in to the lazy compile method, but jumps directly where it needs to go.

Why TenderJIT?

I built this JIT for several reasons. The first, main reason, is that I'm helping to build a more production ready actually-fast-and-good JIT at work called YJIT. I was not confident in my skills to build a JIT whatsoever, so I wanted to try my hand at building one, but in pure Ruby.

The second reason is that I wanted to see if it was possible to write a JIT for Ruby in pure Ruby (apparently it is).

My ultimate goal is to be able to ship a gem, and people can just require the gem and their code is suddenly faster.

I picked the name "TenderJIT" because I thought it was silly. If this project can become a serious JIT contender then I'll probably consider renaming it to something that sounds more serious like "SeriousJIT" or "AdequateCodeGenerator".

How can I help?

If you'd like a low friction way to mess around with a JIT compiler, please help contribute!

You can contribute by adding missing instructions or adding tests, or whatever you want to do!

Lots of TenderJIT internals just look like x86-64 assembly, and I'd like to get away from that. So I've been working on a DSL to hide the assembly language away from developers. I need help developing that and converting the existing "assembly-like" code to use the runtime class.

You can find the DSL in lib/tenderjit/runtime.rb.

Thanks for reading! If you want to help out, please ping me on Twitter or open an issue!

More Repositories

1

initial-v

It's a BMW shifter converted to a Bluetooth Keyboard that you use with Vim
C++
980
star
2

asmrepl

A REPL for x86-64 assembly language
Ruby
866
star
3

rails_autolink

The auto_link function from Rails
Ruby
589
star
4

the_metal

A spike for thoughts about Rack 2.0
Ruby
519
star
5

analog-terminal-bell

A bell for your terminal that is analog
OpenSCAD
482
star
6

fisk

A pure Ruby assembler
Ruby
299
star
7

phuby

phuby wraps PHP in a loving embrace
Ruby
265
star
8

esp8266aq

ESP8266 and Plantower AQ sensor
C++
216
star
9

hana

An implementation of JSON Patch and JSON Pointer for Ruby
Ruby
214
star
10

recma

Pure ruby javascript parser and interpreter.
Ruby
196
star
11

dnssd

Multicast DNS client for ruby! YAY!
Ruby
173
star
12

ruby-glossary

Just a glossary of terms I've found in Ruby source code
171
star
13

enterprise

Make ruby ruby application enter the enterprise with the enterprise gem
Ruby
155
star
14

heap-analyzer

A heap analyzer for MRI that isn't very good.
JavaScript
141
star
15

namecase

Properly case people's names
Ruby
128
star
16

refreshing

A Rails Engine that gives Language Server support to Rails apps
Ruby
115
star
17

uart

Simple serial / UART interface for Ruby
Ruby
109
star
18

tusk

Message busses with Observable API
Ruby
101
star
19

heapfrag

Heap visualizer for Ruby
Ruby
97
star
20

nfc

NFC is a ruby wrapper for the Near Field Communication library.
C
90
star
21

tinygql

A tiny and experimental GraphQL parser in Ruby
Ruby
84
star
22

rubycommitters.org

The source for rubycommitters.org
Ruby
81
star
23

ds9

Wrapper around nghttp2
C
80
star
24

fibur

Concurrent execution during Ruby I/O
Ruby
78
star
25

our_pc

Our Procedure Calls
Ruby
71
star
26

magic_scan

My magic card recognition system
Ruby
69
star
27

av_capture

A wrapper around av_capture on OS X
Objective-C
68
star
28

aarch64

Pure Ruby ARM64 Assembler
Ruby
67
star
29

gda

A SQL parser. It wraps libgda
C
64
star
30

taka

Taka is a DOM (core and html) implementation for Ruby
Ruby
63
star
31

minitest-emoji

View your test output as emoji
Ruby
60
star
32

mmap

A wrapper around mmap
C
57
star
33

neversaydie

NeverSayDie will let you rescue from SEGV's and is evil
Ruby
55
star
34

reloader

A demo of live streams on Rails 4
Ruby
50
star
35

HTML5DeckBuilder

an html5 mtg deck builder
JavaScript
49
star
36

sphero

A ruby gem for the sphero ball
Ruby
45
star
37

dot_vim

my dot vim directory
Vim Script
44
star
38

markup_validity

Test for valid markup with test/unit or rspec
Ruby
44
star
39

defrost

Never let pesky "frozen" objects get in your way again! Use Defrost to remove the frozen state from your objects!
Ruby
42
star
40

my_thing

demo of regression test selection
Ruby
42
star
41

gem_survey

a survey script for gems
Ruby
41
star
42

spectacular

View SNMP in your browser
Ruby
38
star
43

zeroconf

Multicast DNS client and server written in pure Ruby
Ruby
31
star
44

leap_motion

Ruby bindings to the Leap Motion C++ library
C++
30
star
45

paddle

Paddle is an rdoc plugin for emitting epub books suitable for use with iBooks
Ruby
29
star
46

horo

RDoc style extracted from Rails and refactored for RDoc 2.5.x
Ruby
27
star
47

earworm

What is that song? Earworm uses libofa and MusicDNS to tell you.
Ruby
26
star
48

streamdeck-ruby-plugin

Minimal Stream Deck plugin written in Ruby
Ruby
24
star
49

raop

RAOP Client is an Airport Express client written in ruby. It allows you to stream music to an Airport Express from Ruby.
Ruby
24
star
50

lolwut

Demo of SSE and reloading with AC::Live
Ruby
23
star
51

purdytest

Colorized output for minitest
Ruby
23
star
52

dejour

find awesome stuff on the network
Ruby
22
star
53

qrtools

QR Decoder for Ruby. Uses libdecodeqr
C++
22
star
54

playpen

A wrapper around the OS X security library
Ruby
21
star
55

geera

A commandline client for JIRA
Ruby
20
star
56

rjson

An JSON parser written with Racc
Ruby
20
star
57

widen

A library for narrowing and widening characters
Ruby
19
star
58

arghhh

I can't figure out SSL, please help!
Ruby
19
star
59

heap-utils

Just random scripts I'm using for Ruby heap info
Ruby
19
star
60

icanhasaudio

I am in ur computar, encodin' and decodin' ur MP3z.
C
19
star
61

housefire

Dumb campfire clone
Ruby
18
star
62

xml_truth

A collection of XML/HTML parser benchmarks for Ruby
Ruby
18
star
63

tldr

Too Long Didn't Run
Ruby
17
star
64

adequatehq.com

Adequate HQ
17
star
65

stree

A wrapper around libstree
C
16
star
66

allocation_sampler

C
16
star
67

wreckster

A Ruby API for Rexster
Ruby
15
star
68

chicken-yaml

libyaml wrapper for chicken scheme
Scheme
15
star
69

ruby-pprof

A Ruby program for parsing gperftools output
Ruby
14
star
70

uchip

Ruby library for interacting with MCP2221 and MCP2221a
Ruby
14
star
71

betabrite

This is a ruby API for the BetaBrite LED sign
Ruby
14
star
72

orient

Binary protocol implementation for Orient DB and Ruby
Ruby
14
star
73

fsmjs

Simulating fsm in JS
14
star
74

imgur2

Quick hack for uploading to imgur
Ruby
13
star
75

roastr

My Christmas roast
R
13
star
76

zomg

An OMG IDL parser and ruby code generator
Ruby
12
star
77

quail

Zero MQ ruby wrapper
Ruby
12
star
78

jit_buffer

General purpose buffer for use with building JITs
Ruby
10
star
79

hacks

Code for accessing CRuby constants and functions as Ruby
Ruby
10
star
80

meow

Meow gives you access to Growl notifications using RubyCocoa
Ruby
10
star
81

awkward

Produce dot files that represent your object graph
Ruby
10
star
82

libxml2

my fork of libxml2
C
10
star
83

WORF

WORF, the DWARF parser
Ruby
10
star
84

mallcop

libssh2 wrapper
Ruby
9
star
85

milton

Ruby
9
star
86

odinflex

Different utilities that I've written
Ruby
9
star
87

wank

Ruby
9
star
88

remotehash

An OpenDHT client library
Ruby
9
star
89

kedama

A wrapper around ming. Create swf files in ruby using ming.
Ruby
9
star
90

rubypan

Ruby
8
star
91

tree_diff

Parse html using different parsers, then show differences between the generated trees
Ruby
8
star
92

hatstone

A minimal Ruby wrapper for Capstone disassembler
C++
8
star
93

e-zine

My E-Zine
7
star
94

rsat

A pure Ruby SAT solver
Ruby
7
star
95

tenderlove.github.com

My awesome website
HTML
7
star
96

compiler

Proof of concept for converting Journey routes to state tables
Ruby
7
star
97

myhidapi

A wrapper around hidapi for Ruby
C
7
star
98

magic

I found some of my old code from 2004 on a USB stick. This code is not for use, it's just for fun
Roff
7
star
99

hearts

I love hearts!
Ruby
6
star
100

writev

A gem that adds the `writev` method to IO
Ruby
6
star