Ruby
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Methods and Blocks
-
Section 4: Object-Oriented Ruby
-
Section 5: Metaprogramming
-
Section 6: Working with Data and Files
-
Section 7: Ruby Frameworks Overview
-
Section 8: Ecosystem and Testing
-
Section 9: Practical Projects
-
Section 10: Interview Practice
-
Section 11: Data Structures and Algorithms in Ruby
-
Section 12: More Practice Exercises
-
Section 13: Enumerable and Functional Style
-
Section 14: More OOP Practice
-
Section 15: More Testing
-
Section 16: Enumerable and Comparable Modules In Depth
-
Section 17: Ruby Standard Library: Core Utilities
-
Section 18: Ruby Standard Library: Data and Security
-
Section 19: Ruby Standard Library: CLI and Text
-
Section 20: Ruby Networking
-
Section 21: Ruby on Rails Deep Dive
-
Section 22: Ruby Metaprogramming Deep Dive
-
Section 23: Ruby Design Patterns
-
Section 24: Ruby Concurrency
-
Section 25: Ruby Testing Deep Dive
-
Section 26: Ruby Gems and Packaging
-
Section 27: Ruby Performance
-
Section 28: More Practice Exercises
-
Section 29: More Interview Practice
-
Section 30: Rails API Development
-
Section 31: Rails Authentication and Authorization
-
Section 32: Rails Testing Deep Dive
-
Section 33: Rails Performance
-
Section 34: Rails Deployment
-
Section 35: Sinatra and Lightweight Ruby Web Apps
-
Section 36: More Ruby Language Deep Dive
-
Section 37: Ruby 3.x Modern Features
-
Section 38: More Data Structures in Ruby
-
Section 39: More Practical Projects
-
Section 40: Ruby Ecosystem Tools
-
Section 41: More Practice and Review
-
Section 42: Final Practice and Mastery
-
Section 43: Ruby Interview Deep Dive
-
Section 44: Ruby Background Processing Deep Dive
-
Section 45: Ruby GraphQL
-
Section 46: Ruby Object Model Deep Dive
-
Section 47: Ruby Hanami Framework Overview
-
Section 48: Ruby gRPC and Protocol Buffers
-
Section 49: Ruby Data Processing
-
Section 50: Ruby Search Integration
-
Section 51: Ruby File Upload and Media
-
Section 52: Ruby Email and Notifications
-
Section 53: Ruby Admin Panels
-
Section 54: Ruby Feature Flags and Experimentation
-
Section 55: Ruby Monitoring and Observability
-
Section 56: Ruby Docker and Deployment Deep Dive
-
Section 57: Ruby Security Deep Dive
-
Section 58: More Advanced Metaprogramming
-
Section 59: More Final Projects
113: JIT Compilation in Ruby (YJIT)
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:
- Create a file named
sieve.rbwith 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"
- Run the script normally using the standard interpreter:
ruby sieve.rb. Note the time. - Run the script again, but this time enable YJIT:
ruby --yjit sieve.rb. Note the time. - 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.
There are no comments for now.