Ruby VM

The secret power of Ruby 2.6: JIT

The secret power of Ruby 2.6: JIT

by Takashi Kokubun

The video titled "The Secret Power of Ruby 2.6: JIT" presented by Takashi Kokubun at RubyConf 2018 discusses the newly integrated Just-In-Time (JIT) compiler in Ruby 2.6, highlighting its potential for performance enhancement and caveats related to its experimental nature. Takashi Kokubun, a contributor to Ruby development, shares his expertise on optimizing Ruby code through JIT compilation.

Key points covered in the talk include:

- Background of JIT: JIT stands for Just-In-Time compilation, which has recently been introduced to improve Ruby's performance starting with Ruby 2.6.

- Historical Context: The presentation provides a brief history of Ruby's performance evolution, emphasizing how earlier versions utilized tree structures for code parsing, moving to a more efficient approach with the introduction of the Ruby virtual machine.

- JIT Compiler Functionality: The JIT compiler translates Ruby code into native machine code, simplifying execution and dramatically increasing speed. For instance, benchmarks like ‘optcarrot’ demonstrate significant performance upgrades, achieving results significantly faster than Ruby 2.5.

- Optimization Usage: The JIT compiler is not enabled by default; developers need to use specific command-line arguments or set environment variables to utilize it effectively.

- Performance Challenges: While powerful, the JIT compiler presents certain challenges, such as

- Memory Limitations: The JIT can slow down Ruby when it faces numerous methods due to caching constraints. With a limit of 1000 cached methods, performance can degrade in resource-heavy environments.

- Resource Pressure: Server systems with constrained resources, when paired with active JIT compilation, may experience performance degradation, particularly with concurrent processes.

- Trace Points Impact: Using trace points can negatively impact performance, as they introduce overhead during debugging.

- Multiple Method Invocation: Simultaneously optimizing many methods can stress the internals of the JIT, causing substantial slowdowns.

- Handling Internal Exceptions: Too many processes requiring JIT can lead to memory management issues.

- Conclusion: While the JIT compiler can greatly enhance Ruby's performance, its deployment should be approached with caution. Developers are encouraged to monitor their applications and utilize benchmarking tools to identify bottlenecks or performance issues. The ongoing development will aim to refine JIT, increasing its efficacy in production settings, underscoring the importance of community feedback in this iterative process.

This informative session wraps up with an invitation for questions, furthering the dialogue about Ruby's JIT capabilities and optimizations post-Ruby 2.6 release.

00:00:15.619 All right. I'll talk about the JIT compiler in Ruby 2.6. Yes, I'm Takashi Kokubun.
00:00:31.439 From R, my company did previous research for ARM, which was acquired by ARM this year. So now, I'm with ARM, and I'm really committed to maintaining the Ruby interpreter engine. Initially, I focused on performance and now I'm mainly maintaining the JIT compiler.
00:00:50.219 You may not know much about the JIT compiler, but last year I experimented with an optimization idea called the JIT compiler. In April, right after Ruby 2.5 was released last Christmas, I prepared a pull request to merge it overnight, and it was merged in early this year. This provided a much longer time for improvement compared to Ruby 2.6.
00:01:31.320 I've been working on the JIT compiler for about 10 months after the merge, and I'm currently among the top contributors for Ruby 2.6. Thank you. This talk is all about Ruby 2.6 and the release notes, which have a section on the JIT.
00:01:56.240 So, what do you know about JIT? If you know, raise your hands! JIT stands for Just-In-Time compilation, which is a process that hadn't been experienced much before. To explain further, let’s discuss the history of Ruby's internals since version 1.8.
00:02:28.700 Back then, parsing the code would create a tree structure that was traversed. If that tree became very complex or large, it would take longer to traverse. Koji introduced the Ruby virtual machine, which converts that tree into sequential instructions, making it faster than purely traversing a tree.
00:03:02.870 In Ruby 2.6, the JIT compiler compiles the code into native machine code specific to the system it's running on, which simplifies operations. The current virtual machine interprets instructions and handles local variables, but with native code, it can directly handle arguments much faster.
00:03:39.019 So, how can we use the JIT compiler? The optimization is still experimental, and by default, it's not enabled. You need to pass a specific option to start using the JIT compiler or set the environment variable. Let’s get started with some benchmarks.
00:04:14.620 There is a benchmark called ‘optcarrot’ aiming to achieve Ruby 3x3, which is the goal of becoming three times faster than Ruby 2.0. For example, a Famicom emulator could achieve 20 frames per second with Ruby 2.0, and the plan is to increase that to 60 frames per second with Ruby 3.
00:04:36.180 Currently, my machine is a little stronger, so it doesn’t achieve 50 frames per second, but with the JIT compiler, it’s already 2.5 times faster.
00:05:04.419 It’s even faster than the previous Ruby 2.5, by 1.8 times. The bar graphs illustrate this progress, showcasing advancements in interpreter performance. This year, we achieved three times the speed of Ruby 2.5, with only 0.5 remaining.
00:05:30.150 But how about other frameworks like Famicom? I guess nobody is running the Famicom in a production environment. I've received bug reports regarding issues with gem implementations where JIT compilation could create problems.
00:05:58.360 It does clear things up but there are trade-offs to consider, especially with Ruby 2.6 being experimental. It's important to monitor and understand these implications as we continue developing.
00:06:21.510 The first topic is about when Ruby becomes slow with the JIT compiler. The first issue that arises is when there are many methods to optimize, which can lead to slower performance.
00:06:58.360 Previous reports compared methods in Ruby environments where dynamic loading of many methods was becoming a bottleneck, as the JIT compiler manages many cached methods. The maximum number of cached methods is set to 1000 by default, which may seem small for complex applications.
00:07:19.560 This limitation arises due to how the JIT compiler compiles C code and handles dynamic loading. The current implementation called MJIT has memory constraints and performance considerations that become evident when running code with many methods.
00:07:57.840 When many methods are loaded, searching for them requires dealing with a large addressable memory space, which increases lookup times. This problem becomes more pronounced as the number of methods rises.
00:08:26.070 Another situation that can cause Ruby to slow down occurs when methods that need to be invoked continue to increase. Platinum 2.6.0 introduced a feature called JIT compaction, which helps optimize method loading by compacting unused memory.
00:08:54.740 The JIT compaction feature provides a way to limit memory overhead, especially for methods that exceed typical thresholds. While it improves overhead, it still may not be an ideal solution, but is better than uncontrolled memory usage.
00:09:24.370 The pressure on CPU and memory resources can also slow down Ruby's performance. When running the JIT compiler alongside other processes, memory demands may lead to slowdowns if the machine has limited resources.
00:09:57.370 If the server running Ruby doesn't have substantial resources, it could lead to performance degradation, especially in instances where both the JIT compiler and garbage collection are active.
00:10:26.290 Trace points can exacerbate performance issues further. The trace point feature dynamically instruments the program to allow detailed debugging. However, enabling trace points affects performance and current optimizations may not support the trace point feature.
00:10:56.540 In summary, there are situations under which Ruby can slow down when using the JIT compiler. Common occurrences include method overload and the limitations imposed by the JIT's handling of all enabled features. During testing, JIT activation may not trigger expected performance improvements.
00:11:35.920 The second major issue with JIT is handling multiple methods that need to be optimized simultaneously. With too many methods under performance pressure, Ruby can experience significant slowdowns.
00:12:11.780 The experimental JIT compiler in Ruby 2.6 tries to balance speed and performance, but faces challenges on some systems. There needs to be a careful consideration of the types of methods being compiled and their usage patterns in order to optimize effectively.
00:12:56.550 Lastly, issues with internal exceptions can occur when too many processes are running that require the JIT compiler. This can lead to assertions related to memory management and can slow down the overall performance of the Ruby application.
00:13:27.780 When it comes to conclusion, these performance considerations suggest that while the JIT compiler is powerful, it must be used with an understanding of its limitations and capabilities. Benchmarking tools are crucial for identifying bottlenecks.
00:13:50.750 As we move forward with Ruby 2.6, the ongoing development of the JIT compiler will likely bring additional features and optimizations. Community feedback is important for fine-tuning these enhancements to deliver a better experience for users.
00:14:35.800 What's the takeaway? The optimization strategies discussed are effective, but can be scenario-dependent. It's paramount to monitor real-world Ruby applications with JIT enabled to ensure satisfactory performance.