statically compiling ruby with llvm
DESCRIPTION
How RubyMotion makes use of LLVM to statically compile Ruby into machine code. Talk given at the LLVM devroom at FOSDEM 2014.TRANSCRIPT
Statically Compiling Ruby with LLVM
Laurent Sansonetti HipByte
About me• Laurent Sansonetti
• Programming Language Nerd • Founder of HipByte • From Belgium (yay!)
MacRuby
MacRuby• 2007: Project created (as a hobby)
• Replacement for RubyCocoa • Fork of CRuby 1.9
• 2008: Had a beer with Chris Lattner • 2009: Replaced bytecode VM by LLVM JIT • 2011: Left Apple
RubyMotion
RubyMotion• Command-line toolchain for iOS / OS X dev • Implementation of Ruby dialect • Unified Ruby runtime with Objective-C • Static compiler for Ruby into Intel/ARM • Platform for wrappers/libraries ecosystem • Commercial product & sustainable business
RubyMotion• Command-line toolchain for iOS / OS X dev • Implementation of Ruby dialect • Unified Ruby runtime with Objective-C • Static compiler for Ruby into Intel/ARM • Platform for wrappers/libraries ecosystem • Commercial product & sustainable business
Ruby
Ruby• Created in 1995 by Yukihiro Matsumoto (Matz)
• “human-oriented language” • Dynamically typed • Object oriented • Blocks • Exceptions • Garbage collection • …
hello.rbclass Hello def initialize(something) @something = something end def say puts “Hello “ + @something end end !Hello.new(‘world’).say
CRuby
Compilation
AST
Bytecode
Ruby Code
Runtime
Let’s use LLVM!
RubyMotion
Compilation
AST
LLVM IR
Assembly
Ruby Code
Runtime
RubyMotion compiler• About 12k C++ LOC • Targets LLVM 2.4 • Supports the entire Ruby language “specifications”
File functionsclass Hello def initialize(something) … end def say … end end !class Ohai < Hello def say … end end !Hello.new(‘world’).say
File Scope
Hello Ctor
Ohai Ctor
Hello Scope
Ohai Scope
File Code
1
2
53
4
Method functions
class Hello … def say puts “Hello “ + @something end end
Method IMP
ObjC Stub ObjC Runtime
Ruby Runtime
Methodsdef hello(x, y, z) … end
Methods
define internal i32 @"rb_scope__hello:__"(i32 %self, i8* %sel, i32 %a, i32 %b, i32 %c) { MainBlock: … }
Conditionalsdef hello(something) if something true else false end end
Conditionalsdefine internal i32 @"rb_scope__hello:__"(i32 %self, i8* %sel, i32 %something) { MainBlock: call void @llvm.dbg.declare(metadata !{i32 %self}, metadata !23), !dbg !54 call void @llvm.dbg.value(metadata !{i32 %something}, i64 0, metadata !24), !dbg !54 %0 = alloca i8 store volatile i8 0, i8* %0 switch i32 %something, label %merge [ i32 0, label %else i32 4, label %else ] !else: ; preds = %MainBlock, %MainBlock br label %merge !merge: ; preds = %MainBlock, %else %iftmp = phi i32 [ 0, %else ], [ 2, %MainBlock ] ret i32 %iftmp }
Local variables• Allocated on the stack
• Benefits from the mem2reg pass
Block variables• Allocated initially on the stack • Re-allocated in heap memory in case the block
leaves the scope of the method
kernel.bc• Runtime primitives
• vm_fast_{plus,minus,…} (arithmetic ops) • vm_ivar_{get,set} (instance variables) • vm_dispatch (method dispatch) • …
• Pre-compiled into LLVM bitcode • Loaded by the compiler
• Provides the initial module
Instance variablesdef initialize(foo) @foo = foo end
Instance variablesdefine internal i32 @"rb_scope__initialize:__"(i32 %self, i8* %sel, i32 %foo) { MainBlock: %0 = alloca i32* %1 = alloca i32 store i32* %1, i32** %0 store i32 %foo, i32* %1 %2 = alloca i8 store volatile i8 0, i8* %2 br label %entry_point !entry_point: %3 = load i32** %0 %4 = load i32* %3 %5 = load i32* @3 %6 = load i8** @4 call void @vm_ivar_set(i32 %self, i32 %5, i32 %4, i8* %6) ret i32 %4 }
Instance variablesPRIMITIVE void vm_ivar_set(VALUE obj, ID name, VALUE val, void *cache_p) { … klass = *(VALUE *)obj; if (klass == cache->klass) { if ((unsigned int)cache->slot < ROBJECT(obj)->num_slots) { rb_object_ivar_slot_t *slot; slot = &ROBJECT(obj)->slots[cache->slot]; if (slot->name == name) { … GC_WB_OBJ(&slot->value, val); return; … // slow path PRIMITIVE VALUE
vm_gc_wb(VALUE *slot, VALUE val) { … *slot = val; return val; }
After passes
Instance variablesdefine internal i32 @"rb_scope__initialize:__"(i32 %self, i8* %sel, i32 %foo) { MainBlock: br label %entry_point !entry_point: … %39 = getelementptr inbounds %struct.rb_object_ivar_slot_t* %28, i32 %26, i32 1 store i32 %4, i32* %39 … ret i32 %4 }
Arithmeticdef answer 21 + 21 end
Arithmeticdefine internal i32 @rb_scope__answer__(i32 %self, i8* %sel) { MainBlock: br label %entry_point !entry_point: %0 = load i8** @8 %1 = load i8* @9 %2 = call i32 @vm_fast_plus(i32 85, i32 85, i8 %1) ret i32 %2 }
Arithmetic
PRIMITIVE VALUE vm_fast_plus(VALUE left, VALUE right, unsigned char overridden) { if (overridden == 0 && NUMERIC_P(left) && NUMERIC_P(right)) { if (FIXNUM_P(left) && FIXNUM_P(right)) { const long res = FIX2LONG(left) + FIX2LONG(right); if (FIXABLE(res)) { return LONG2FIX(res); } } } … // slow path }
After passes
Arithmetic
define internal i32 @rb_scope__answer__(i32 %self, i8* %sel) { MainBlock: br label %entry_point !entry_point: … ret i32 169 }
Exceptions• Implemented as C++ exceptions • Zero-cost for “normal flow” • Handlers are compiled using IR intrinsics
• “catch all” landing pad clause • Exception#raise triggers __cxa_raise()
DWARF• All instructions have proper debug location
metadata • Method/block arguments and local variables are
tagged as DW_TAG_{arg,auto}_variable • Build system generates a .dSYM bundle
• Can be loaded by gdb/lldb, atos(1), profilers, etc.
REPL• Allows to interpret expressions at runtime
• Only for development (simulator) • App process loads the compiler • Uses JIT execution engine
Demo
LLVM lessonsPluses • Great to write static
compilers • Easy to target new
platforms • Lots of great
optimization passes
Minuses • C++ API breakage • Huge code size • IR is not 100% portable • Proprietary backends • Not as great to use as a JIT
LLVM is awesome!