Skip to Content
Course content

113: JIT Compilation in Ruby (YJIT)

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Imagine you're a chef following a complex recipe for the first time. You have to stop every few seconds to read the next line: "Add two grams of salt," then "Stir for thirty seconds." You're moving slowly because you're interpreting the instructions in real-time. But after making that same dish a hundred times, you don't need the book anymore. You've internalized the process. Your hands just move automatically; the "instructions" have been compiled into muscle memory. You're now operating significantly faster because you've skipped the step of reading and translating the recipe.

This is exactly what's happening when we talk about YJIT (Yet Another Just-In-Time compiler) in Ruby. Traditionally, Ruby uses YARV (Yet Another Ruby VM), which acts like that chef reading the recipe. It turns your code into bytecode and interprets it. YJIT sits on top of that. It watches your code as it runs, identifies the "hot" parts—the methods being called thousands of times—and compiles them directly into machine code. It transforms the "recipe" into "muscle memory" for your CPU.

The Magic of Skipping the VM Dispatch

In a standard Ruby execution, every time you call a method, the VM has to do a lot of bookkeeping. It asks: "What class is this? Does this method exist? Where is it defined?" Even if you've called that method a million times in a loop, the VM usually does a version of that check every single time.

YJIT changes the game by making assumptions based on what it's seen. If YJIT notices that a specific loop always calls .each on an Array of Integers, it generates machine code that assumes the object will always be an Array of Integers. It skips all that bookkeeping and jumps straight to the execution. If the assumption turns out to be wrong (e.g., you suddenly pass a String instead), it simply "de-optimizes" and falls back to the standard interpreter. It's a high-reward gamble that almost always pays off in production environments.

Seeing the Speed Difference

You don't need to change a single line of your code to use YJIT, but you do need to tell Ruby to turn it on. I usually do this via a flag when starting the process. For example:

ruby --yjit my_script.rb

Now, here is a bit of a reality check: you won't see a massive difference in a script that just prints "Hello" or fetches one record from a database. YJIT is designed for "hot" code. If your script spends 99% of its time waiting for a database query to return (I/O bound), YJIT won't help you much. But if you're doing heavy data transformation, complex mathematical calculations, or running a massive Rails app with thousands of method calls per request (CPU bound), the performance bump is noticeable.

I remember the first time I enabled YJIT on a heavy background job worker. We saw a throughput increase of nearly 15-20% without changing a single line of logic. It felt like a cheat code.

When YJIT Might Not Be Your Friend

I'll be honest with you: there's a trade-off. Because YJIT is compiling code and storing that machine code in memory, your RAM usage will go up. You're trading memory for speed. In a tiny container with very strict memory limits, this could actually lead to OOM (Out of Memory) errors if you aren't careful. Most of the time, the trade-off is worth it, but it's something to keep on your radar when tuning your production environment.




📋 Practical Task

Comparing YJIT Execution on a Prime Number Sieve

To actually feel the difference YJIT makes, you need a CPU-intensive task. We're going to write a script that calculates all prime numbers up to 500,000 using a basic sieve, then compare the runtime with and without YJIT.

Your Task:

  1. Create a file named sieve.rb with the following code:
require 'benchmark'

def find_primes(limit)
  primes = Array.new(limit + 1, true)
  primes[0] = primes[1] = false

  (2..Math.sqrt(limit)).each do |i|
    if primes[i]
      (i*i..limit).step(i) { |j| primes[j] = false }
    end
  end
  
  primes.each_with_index.select { |p, i| p }.map { |p, i| i }
end

time = Benchmark.realtime do
  find_primes(500_000)
end

puts "Completed in #{time.round(4)} seconds"
  1. Run the script normally using the standard interpreter:
    ruby sieve.rb. Note the time.
  2. Run the script again, but this time enable YJIT:
    ruby --yjit sieve.rb. Note the time.
  3. Observe the difference. You should see a measurable decrease in execution time when YJIT is enabled, as the inner loops of the sieve are "hot" and get compiled to machine code.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.