Ruby is dead; Long live Yet Another Ruby JIT

Ruby is dead ? Just not yet, earlier this year Shopify released their Ruby 3.2 YJIT improvement as production ready. Ruby has been taking a beating this decade compared to languages such as Elixir, Go, Crystal and Rust when it came to speed and performance. At Wego we have been a dominant ruby shop in the past but with a move from monolith to service oriented architecture opened new venues and teams have picked languages that suited their strength and use cases varying from Elixir, Crystal, Go, Rust and Java. Still some of our core services continue to run in Ruby and maintained well. Nothing still beats ruby when it comes to the ease of writing code and the ecosystem. Over Rails I am personally skeptic over its ease of use with so much magic now as part of the framework but this ain't a RoR post so we will focus on Ruby with YJIT ;).

Initially I was skeptic over the gains to be achieved for our API based services with YJIT. By the way YJIT stands for Yet Another Ruby JIT.

We upgraded our projects from 3.1.3 to 3.2.2. using docker.io/ruby:3.2.2-slim-bullseye . Using Ruby 3.2.2 itself adds no spice to the mix and it just runs as regular ruby with no apparent gains. The magic happens after you enable the YJIT, which can be done by setting the environment variable RUBY_YJIT_ENABLE=true.

The latency dropped by over 30% with YJIT enabled upon deploy without moving any knobs or tuning anything in the code base for p50, p75, p90 and even p95. You can see the dip and consistently maintained lower latency over a month to rule out any fresh deploys false gains.

Though the gains were impressive it came with a downside a massive appetite for memory. Almost 60% more memory consumption for maximum memory from before with default YJIT settings.

The default size of the executable memory block to allocate in YJIT is 64MB by default. Though as per Shopify the memory usage is comparitively less than using YJIT with ruby 3.1 it was still high compared to no YJIT enabled. As YJIT's power comes from the JIT compiler over interpreters using memory overhead.

This is not a deal breaker as you can define value for the executable memory block and override the default value of 64MB by setting the flag --yjit-exec-mem-size. What worked best for us was setting it to 16MB and reduce memory usage and still keep the latency gains of over 30%.

           --yjit-exec-mem-size=16

 

Ruby is very much alive and kicking with YJIT. The performance gains are real and worth upgrading and adopting ruby for your future projects.

View Comments