Supercharging Rails: Enabling Ruby's YJIT Compiler in Production
In recent years, the Ruby community has eagerly awaited the introduction of the YJIT (Just-In-Time) compilation engine, a feature that promised to significantly enhance Ruby's performance by converting Ruby bytecode into machine code just before execution. For Rails applications, this means the possibility of improved response times and reduced server load, making YJIT a compelling option for production environments. This post aims to walk you through the process of enabling YJIT in your Rails application to capitalize on these performance enhancements.
Understanding YJIT
Before we dive into the setup process, it's important to understand what the YJIT project is and how it represents a departure from Ruby's previous efforts with Just-In-Time compilers. As a method-based JIT compiler integrated into the CRuby interpreter, YJIT is designed to be more efficient and practical for real-world applications, aiming to reduce the overhead typically associated with JIT compilation. Benchmarks performed by all major companies over the last year have been promising, showcasing YJIT as a valuable asset for Rails applications looking to boost performance.
For a full background, Shopify's engineering blog here provides an in-depth look at the YJIT compiler's development and the progress made by the team. The project is also open-source, and anyone curious can read the full documentation and even the source code itself on Github here.
Preparing Your Application for YJIT
Although YJIT was made publicly available in Ruby 3.1.0, we recommend updating to Ruby version 3.2.0 and Rails version 6.0+ as both of those versions include a number of changes to the YJIT compiler.
From Ruby 3.3.0 and Rails 7.1.0 onwards, YJIT will be enabled by default, simplifying the process for those able to upgrade to these versions. If your project isn't on these versions yet, we suggest prioritizing these upgrades and allowing some time for stabilization in production before activating YJIT.
Testing With YJIT Enabled
Because the YJIT compiler is a new and potentially dangerous technology, it's very important that you do full regression testing before deploying. To begin testing in your local environment, first ensure that your local ruby version has yjit enabled by running the following command and you'll likely see one of the two responses:
# Success
> ruby --yjit -v
ruby 3.2.2
# Error: Rust dependency is missing
> ruby --yjit -v
warning: Ruby was built without YJIT support. You may need to install rustc to build Ruby with YJIT.
If you get the dependency error for rustc, then install rust locally, uninstall your current version of ruby and re-install. An example of the commands are below:
> curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
> source $HOME/.cargo/env
> rustc --version
rustc 1.77.2 (25ef9e3d8 2024-04-09)
> asdf uninstall ruby 3.2.2
> asdf plugin-update ruby
> asdf install ruby 3.2.2
> ruby --yjit -e "p RubyVM::YJIT.enabled?"
true
Now that the YJIT compiler is configured locally, run your automated test suite and perform local regression testing following your normal testing procedure.
Enabling YJIT in your Deployed Environments
After you are confident in your regression testing, you can move to activating YJIT in your deployed environments, which can be accomplished through an ENV variable or directly within your application's code. YJIT is compatible with popular Ruby servers like Puma, Unicorn, and Webrick. Although there has not been extensive testing with servers such as Falcon or Thin, YJIT is expected to work seamlessly across different environments. Encountering issues with any server should prompt a bug report to help ensure YJIT's broad compatibility.
For those preferring an environment variable setup, you can enable YJIT in your production environment with the following configurations:
# In production server configurations
export RUBYOPT="--yjit"
# In Dockerfile
ENV RUBYOPT="--yjit"
# In binstubs, e.g. bin/rails
RUBYOPT="--yjit"
Ruby 3.3.0 users have an additional option, thanks to YJIT's paused-by-default state, allowing for initialization within the application. More information on this feature can be found on GitHub. An initializer might look like this:
# In production server configurations
if defined?(RubyVM::YJIT) && RubyVM::YJIT.respond_to?(:enable)
RubyVM::YJIT.enable
else
puts "YJIT is not enabled"
end
Deployment and Monitoring
Once YJIT is enabled, deploy your application as you normally would, but pay close attention to performance changes. Utilize performance monitoring tools to track improvements or identify potential issues post-deployment.
Anticipated Performance Gains
Implementing YJIT may alter memory usage, affecting your server's process and thread configuration. While YJIT can significantly enhance performance, especially for CPU-intensive applications with adequate memory, its effectiveness can vary. It's currently supported on x86 architecture for macOS and Linux, with notable speed increases reported in benchmarks.
While development environments may not see dramatic benefits due to Rails' code reloading behavior, YJIT still holds potential for applications with heavily used library methods. Testing and experimentation are key to evaluating YJIT's impact on your development process.
Conclusion
YJIT marks a significant advancement in Ruby's development, offering Rails applications a path to improved performance. By following this guide to enable YJIT, you could see faster response times and a more fluid user experience. With the Ruby community's ongoing efforts to refine YJIT, keeping abreast of new developments will ensure your application benefits from the latest performance optimizations. This guide aims to equip you with the knowledge to leverage YJIT's advantages, fostering a more efficient and responsive production environment.