Fast Ruby
I Feel the Need—the Need for Speed in Ruby!
Ruby’s flexibility is one of its superpowers, offering multiple ways to perform the same operation. While some methods are more elegant or maintainable, others prioritize speed. Like in any programming language, optimizing for performance sometimes means selecting a better algorithm—for instance, avoiding the bogosort algorithm is a given.
Before diving into code architecture improvements, let’s explore how to make our Ruby code faster with the fasterer
gem, a tool that performs static code analysis to identify potential speed gains. Its a little older, but still worth reviewing.
https://github.com/DamirSvrtan/fasterer
An additional resource is the rubocop-performance
gem, offering similar functionality by suggesting faster code alternatives.
https://github.com/rubocop/rubocop-performance
For those looking to push boundaries further, consider offloading heavy computations to a compiled language. This can be achieved by integrating compiled libraries into your Ruby application via the ffi
(foreign function interface).
Another noteworthy option is enabling Ruby’s Just-In-Time (JIT) compiler. With Shopify’s endorsement for production use, it’s a viable choice for many.
https://shopify.engineering/ruby-yjit-is-production-ready
While JIT offers significant gains by selectively compiling code, some scenarios, like REPLs, benefit from interpretation.
The quest for a fully compiled Ruby application brings us to Crystal, a language with syntax closely mirroring Ruby’s, offering the benefits of static typing without mandatory type declarations. Crystal’s compatibility with Ruby syntax raises the possibility of dual-runtime codebases.
Efforts to automate Ruby-to-Crystal conversions are underway, with gems like synvert
paving the way, despite not providing a complete solution and requiring some manual adjustments.
https://github.com/synvert-hq/synvert-ruby
The newly announced crystalruby
gem sparks excitement for potentially faster Ruby code by blending Crystal code within Ruby projects, promoting an intriguing pathway for performance optimization.
https://github.com/wouterken/crystalruby
A conversation on Ruby performance enhancements isn’t complete without mentioning JRuby. Ruby’s design is adaptable across various implementations, with most familiarly running on the Matz’s Ruby Interpreter (MRI) in C - sometimes called CRuby. JRuby offers a Java-based alternative, compatible with the Java Virtual Machine (JVM), and capable of integrating with other JVM languages. This fusion enables diverse and complex applications, possibly converting Java enthusiasts to Ruby due to its simplicity and productivity.
The most remarkable JRuby version for speed is offered by TruffleRuby, leveraging the JVM’s capabilities for enhanced performance.
Enhancing Ruby’s execution speed isn’t merely about individual optimizations—it’s about considering the broader architecture and utilizing the right tools and practices based on your application’s specific needs. Constantly profiling your application and keeping abreast with the latest developments in the Ruby community are key to maintaining and improving the performance of Ruby applications.
It is crucial to underline the importance of understanding the underlying mechanics of the Ruby interpreter you’re using, whether it’s MRI, JRuby, or any other. Different interpreters might offer unique optimizations that can be leveraged to speed up your Ruby code.
Code Optimization Techniques
-
Avoid Creating Unnecessary Objects: Ruby objects consume memory. Reusing objects or employing symbols instead of strings when the value doesn’t change can save memory and CPU cycles.
-
Benchmark and Profile Your Code: Utilize Ruby’s built-in libraries such as
benchmark
andruby-prof
to identify bottlenecks in your code. This empowers you to make informed decisions on where optimization efforts should be directed.require 'benchmark' puts Benchmark.measure { # Your code here }
-
Garbage Collection Tuning: Ruby’s garbage collector is configurable. Adjusting its parameters, like
RUBY_GC_HEAP_INIT_SLOTS
andRUBY_GC_MALLOC_LIMIT
, can help in managing application memory usage more efficiently and can lead to performance improvements, especially for larger applications. -
Use Caching Wisely: Memoization or other caching strategies can significantly reduce method call times for computationally intensive operations. However, caching should be applied judiciously to avoid stale data and excessive memory use.
-
Multithreading and Concurrency: MRI has global interpreter lock (GIL), which limits threads to executing one at a time. However, utilizing Ruby’s Thread class or external libraries like
concurrent-ruby
for I/O-bound operations can lead to performance improvements. For CPU-bound work, consider using processes instead of threads or switch to a Ruby implementation without GIL, such as JRuby. -
Eager Loading: In Rails applications, inefficient database queries can significantly impact performance. Eager loading associated records using the
includes
method can reduce the number of database calls.
Example: Efficient Array Processing
Consider the task of finding intersection elements between two arrays, which can be slow for large datasets if not optimized:
# Non-optimized way
intersect = array1.select { |element| array2.include?(element) }
# Optimized way
require 'set'
set2 = array2.to_set
intersect = array1.select { |element| set2.include?(element) }
Converting one of the arrays to a set reduces the time complexity of lookups, resulting in significant performance gains for large arrays.
Consider Writing Extension Libraries
For extremely performance-critical sections, writing a Ruby extension in C or Rust can dramatically boost performance. This approach allows for direct manipulation of Ruby objects and can tap into the speed of compiled languages. However, this should be reserved for hotspots within your application, as maintaining extensions adds complexity.