Talks

Making .is_a? Fast

Making .is_a? Fast

by John Hawthorn

In his talk "Making .isa? Fast" at RubyConf 2022, John Hawthorn discusses the performance bottleneck associated with the `isa?method in Ruby, which checks an object's class or module membership. Despite its frequency of use in Ruby applications—accounting for roughly two percent of total runtime—the performance ofisa?has historically fallen short of expectations. Hawthorn begins by explaining theisa?method's functionality, illustrating its usage with simple examples involving class checking. He emphasizes that profiling is essential for identifying performance issues, stating that in certain production environments, theis_a?` method can consume up to four percent of runtime.

Key points from Hawthorn's presentation include:
- Profiling Tools: He recommends tools like stack Prof and rbspy for performance profiling, revealing that multiple methods, including === and case statements, contribute to runtime overhead.
- Performance Analysis: He analyzes how Ruby’s checking of class ancestry can be inefficient due to issues like linked list structures causing increased time for checks, particularly when classes include multiple modules, which can complicate the ancestor hierarchy.
- Proposed Solutions: Hawthorn suggests enhancing the implementation of is_a? to achieve constant time performance, which involves reworking the Ruby VM to optimize this method by reducing reliance on iterative checks.
- Memory Optimization: The approach includes the usage of a hash to manage class ancestors efficiently, which not only streamlines checks but also reduces overall memory usage.
- Community Engagement: He urges the Ruby community to engage in performance profiling and to contribute insights that can benefit the ecosystem.

Hawthorn concludes by highlighting the significant improvements made through these optimizations, which allow Rails applications to operate more efficiently. His call to action encourages collective efforts in enhancing Ruby's performance landscape, particularly through profiling and optimization strategies that simplify the use of is_a? without disrupting existing application logic.

00:00:00.299 foreign
00:00:13.099 I'm really excited for Ruby company and I'm thrilled to be a part of it. My name is John Hawthorn, my pronouns are he/him, and I'm on the Ruby 14 release support team. I'm a staff software engineer on the Ruby architecture team at Game Mode. Largely, I love working on performance and related topics.
00:00:18.000 You can find me online at @jhawthorn on platforms like GitHub and Twitter, if Twitter is still online. I've also been enjoying Mastodon on ruby.social, so feel free to pick that up if you haven't already.
00:00:51.480 This talk is about the `is_a?` method, which you can call on any object to determine whether that object is of a specific class or module. The technical term usually used is whether it implements a class or module. Essentially, it checks if there is an exact match or if it checks the inheritance chain.
00:01:09.659 Let's look at a few quick examples on the same page: if we ask if the string 'foo' is a string, the answer is yes. If we ask if the integer 1 is a string, the answer is no. If we ask if the integer 1 is an integer, the answer is yes, which is true. However, since an integer is also a numeric, the `is_a?` method checks the class ancestry, confirming that integer is indeed a subclass of numeric.
00:01:43.439 I realize these examples might seem ridiculous, similar to that outdated meme where a guy asks whether a butterfly is a pigeon. In this case, we have our Ruby version, which is quite fitting. People intuitively understand class relationships, so it's odd that this could be a performance bottleneck.
00:02:08.759 However, the Ruby VM, like any computer, needs a strict definition of how to determine these relationships. While researching this, I found out that the `is_a?` method has efficiently been applied in other programming environments.
00:02:30.620 There are other methods that accomplish similar functionalities, such as the triple equals method (`===`), case statements, and rescue statements, all of which utilize a class inheritance check when called. These methods are frequently used, and because of their prevalence, I've found that they collectively account for about two percent of the total Ruby runtime. This can vary depending on the application, but I consistently see this as a stable metric.
00:03:56.099 Profiling is incredibly valuable; if you're not profiling, you're essentially guessing at what's slow, which is a common pitfall on certain websites. I typically use `stack Prof`, a sampling Ruby profiler, as well as `rbspy`, both of which are excellent tools.
00:04:46.259 We've utilized RailsBench, a benchmarking tool for a simple Rails app with a couple of endpoints. Running `stack Prof` against this application shows that the module triple equals takes around 1.7 percent of the runtime. Additionally, in production environments like GitHub, the `is_a?` method was taking almost four percent of the total time, which is quite significant.
00:05:54.360 Going into more detail about this profiling, I leveraged Linux Prof, a great tool for fast and efficient performance profiling, even running it in production environments. Using this tool for our load tests and on the GitHub application yielded similar results.
00:06:20.220 What I found during the profiling was that methods such as `RB_Object_kind_of?` (which check the object's class and its ancestry) are executed frequently. Understanding this gave me insight into how Ruby handles objects and methods.
00:06:45.600 Performance issues can arise when dealing with linked lists and their data structure in memory, especially considering that modern CPUs prefer tightly packed data within cache lines for better efficiency.
00:07:16.320 In particular, Ruby's way of checking class ancestry, by iterating through both class and module ancestors, can be inefficient. The memory layout affects performance—each check consumes more time due to the dispersed memory structure associated with linked lists.
00:08:33.900 Another important thing to note is that when modules are included in classes, the actual number of ancestors can surmount to a complex hierarchy. While the unique feature of Ruby allows you to include base classes into new structures, the practice can bloat class ancestry, which can lead to performance degradation.
00:09:21.660 The ideal solution would be to enhance the `is_a?` implementation rather than simplifying the class hierarchy, which might be evidence of sound engineering practices. Additionally, avoiding method calls to `is_a?` where possible and using alternatives like `instance_of?` can help mitigate this performance issue.
00:10:00.000 Ultimately, since the performance of `is_a?` tends to be slower than we anticipate, it would be beneficial to modify Ruby's internal code to optimize it—creating an assembly language version of `is_a?` would mean it could run in constant time.
00:11:45.300 Upon implementing these performance improvements within the Ruby VM, one of the approaches I took involved using a hash to store the ancestors in such a manner that we could easily check against them.
00:12:55.200 After iterating through numerous methods and determining the best approach, I ended up with an optimized constant-time implementation for the `is_a?` method based on class depth comparisons, reducing the need for iteration altogether.
00:14:15.120 The new implementation reduces the memory footprint significantly as well, making it less burdensome on resources, hence improving overall application performance.
00:15:36.900 Through sharing memory across classes based on common inheritance paths, we can ensure that we save on space and optimize performance, allowing Rails applications to run more smoothly with significant improvements.
00:17:00.120 Finally, the new implementation simplifies the use of `is_a?` across the Ruby ecosystem, allowing for faster checks without altering library or application logic significantly. I hope to inspire the community to participate in performance profiling and sharing those insights.
00:17:52.260 Thank you for your attention! I look forward to seeing how we can all work together to enhance Ruby's performance and usability.