Compiling Ruby to Native Code with Sorbet and LLVM

Jake Zimmerman (@jez)
Trevor Elliott (@elliottt)

November 10, 2021

Sorbet


+

LLVM


=

the Sorbet Compiler

Agenda

  • Why does Stripe care about performance?

  • Why build a compiler for Ruby?

  • How does it work?

  • How are we adopting it?

Agenda

  • Why does Stripe care about performance?

  • Why build a compiler for Ruby?

  • How does it work?

  • How are we adopting it?

📈 Stripe is an API for building a business

  • Accept payments
  • Coordinate payouts
  • Manage taxes

API speed is a feature ✨

  • Stripe users want lower latency

  • Stripe API runs on every checkout

Agenda

  • Why does Stripe care about performance?

  • Why build a compiler for Ruby?

  • How does it work?

  • How are we adopting it?

💎 Stripe uses Ruby extensively

  • Powers our most important services (Stripe API)

  • Hundreds of engineers use Ruby daily

  • Millions of lines of code (monorepo)

  • Massive type coverage with Sorbet

Visualizing API Latency

Visualizing API Latency

Why not TruffleRuby or JRuby?

  • No incremental migration

  • Compiler works with existing Ruby VM

Why AoT, not JIT?

AoT: ahead-of-time
JIT: just-in-time

  • Sorbet types speed up generated code

  • AoT are simpler (implement, debug)

  • Can still do both!

Agenda

  • Why does Stripe care about performance?

  • Why build a compiler for Ruby?

  • How does it work?

  • How are we adopting it?

A Ruby program





def f(x)
  x.map {|v| v + 1}
end

A Ruby program with Sorbet

sig do
  params(T::Array[Integer])
  .returns(T::Array[Integer])
end
def f(x)
  x.map {|v| v + 1}
end
# srb tc
No errors! Great job.

Catching a type error

sig do
  params(T::Array[Integer])
  .returns(T::Array[Integer])
end
def f(x)
  x.map {|v| v + 1}.to_s
end #              ^^^^^
# srb tc
editor.rb:6: Expected `T::Array[Integer]`
but found `String` for method result type
     6 |  x.map {|v| v + 1}.to_s
          ^^^^^^^^^^^^^^^^^^^^^^

LLVM

  • Compiler backend toolkit

  • Used by many compilers:
    Clang, GHC (Haskell), Swift, …

A simple Ruby program

# my_lib.rb

def foo(val)
  puts val
end

A simple Ruby program in C

# my_lib.rb

def foo(val)
  puts val
end

VALUE my_foo(VALUE self, VALUE val) {
  return rb_funcall(self, rb_intern("puts"), 1, val);
}

void Init_my_lib() {
  rb_define_method(rb_cObject, "foo", my_foo, 1);
}

Compiling the example


sig do
  params(T::Array[Integer])
  .returns(T::Array[Integer])
end
def f(x)
  x.map {|v| v + 1}
end


Compiling the example

# compiled: true
sig do
  params(T::Array[Integer])
  .returns(T::Array[Integer])
end
def f(x)
  x.map {|v| v + 1}
end


The Compiler’s View

# compiled: true
# sig do
#   params(x: T::Array[Integer])
#   .returns(T::Array[Integer])
# end
def f(x)
  raise unless x.is_a?(Array)
  t = x.map {|v| v + 1}
  raise unless t.is_a?(Array)
  t
end

Avoiding VM Dispatch

# compiled: true
def f(x)
  raise unless x.is_a?(Array)
  t = rb_ary_collect(x) do |v|
    v + 1
  end
  raise unless t.is_a?(Array)
  t
end

Inlining rb_ary_collect

# compiled: true
def f(x)
  raise unless x.is_a?(Array)
  t = []; i = 0; len = x.length
  while i < len
    t << <callblock>(x[i]) {|v| v + 1}
    i += 1
  end
  raise unless t.is_a?(Array)
  t
end

Inlining the block

# compiled: true
def f(x)
  raise unless x.is_a?(Array)
  t = []; i = 0; len = x.length
  while i < len
    t << x[i] + 1
    i += 1
  end
  raise unless t.is_a?(Array)
  t
end

Avoiding VM Dispatch

# compiled: true
def f(x)
  raise unless x.is_a?(Array)
  t = []; i = 0; len = x.length
  while i < len
    t << x[i] + 1
    i += 1
  end
  raise unless t.is_a?(Array)
  t
end

Removing redundant type tests

# compiled: true
def f(x)
  raise unless x.is_a?(Array)
  t = []; i = 0; len = x.length
  while i < len
    t << x[i] + 1
    i += 1
  end
  raise unless t.is_a?(Array)
  t
end

The final version

# compiled: true
def f(x)
  raise unless x.is_a?(Array)
  t = []; i = 0; len = x.length
  while i < len
    t << x[i] + 1
    i += 1
  end

  t
end

The final version approaches C

# compiled: true
def f(x)
  raise unless x.is_a?(Array)
  t = []; i = 0; len = x.length
  while i < len
    t << x[i] + 1
    i += 1
  end

  t
end
VALUE rb_f(VALUE x) {
  if (!RB_TYPE_P(x, T_ARRAY))
    rb_raise(rb_eRuntimeError);
  VALUE t = rb_ary_new2(0);
  int len = RARRAY_LEN(x);
  for (int i = 0; i < len; i++) {
    VALUE v = FIX2INT(rb_ary_entry(x,i));
    rb_ary_push(t, INT2FIX(v + 1));
  }
  return t;
}

Agenda

  • Why does Stripe care about performance?

  • Why build a compiler for Ruby?

  • How does it work?

  • How are we adopting it?

Goals for adoption

1️⃣ Plan for when things go wrong

2️⃣ Compare performance on real traffic

3️⃣ Must be incremental

1️⃣ Plan for when things go wrong

  • Compiler test cases

  • Entire Stripe test suite

  • Pre-production (staging environment)

  • Blue/green deploys

  • Separate host set to kill traffic fast

2️⃣ Performance on real traffic

3️⃣ Must be incremental

→ Measure with Stackprof

What’s next? 💭

📈 Increase adoption
  (fraction running compiled)

Profile and optimize
  (improve compiled performance)

Questions? 🙋

btw, we have stickers!

also, we’re hiring!

stripe.com/jobs