Ruby Performance Optimization Guide
Performance optimization is an important topic in Ruby development. Although Ruby is known for developer productivity, we can significantly improve application performance with reasonable optimization techniques.
📋 Chapter Contents
- Performance Analysis Tools
- Memory Optimization Techniques
- Code Optimization Strategies
- Database Query Optimization
- Caching Strategies
- Concurrency and Asynchronous Processing
🔍 Performance Analysis Tools
Benchmark Module
ruby
require 'benchmark'
# Simple benchmark test
time = Benchmark.measure do
1000000.times { "hello".upcase }
end
puts time
# Compare different implementations
Benchmark.bm(10) do |x|
x.report("String#upcase:") { 1000000.times { "hello".upcase } }
x.report("String#swapcase:") { 1000000.times { "hello".swapcase } }
x.report("String#downcase:") { 1000000.times { "HELLO".downcase } }
end
# More detailed comparison
Benchmark.bmbm do |x|
x.report("Array#each") do
arr = (1..10000).to_a
arr.each { |i| i * 2 }
end
x.report("Array#map") do
arr = (1..10000).to_a
arr.map { |i| i * 2 }
end
endruby-prof Profiler
ruby
# Install: gem install ruby-prof
require 'ruby-prof'
# Start profiling
RubyProf.start
# Code to analyze
def slow_method
1000000.times do |i|
Math.sqrt(i)
end
end
slow_method
# Stop analysis and get results
result = RubyProf.stop
# Output results
printer = RubyProf::FlatPrinter.new(result)
printer.print(STDOUT)
# Generate HTML report
printer = RubyProf::GraphHtmlPrinter.new(result)
File.open("profile.html", "w") { |file| printer.print(file) }memory_profiler Memory Analysis
ruby
# Install: gem install memory_profiler
require 'memory_profiler'
report = MemoryProfiler.report do
# Code to analyze
1000.times do
"hello world".upcase
end
end
report.pretty_print🧠 Memory Optimization Techniques
1. Use Symbols Instead of Strings as Hash Keys
ruby
# Inefficient: String keys create multiple objects
bad_hash = { "name" => "John", "age" => 30 }
# Efficient: Symbols are immutable, only one instance in memory
good_hash = { name: "John", age: 30 }
# Benchmark test
require 'benchmark'
Benchmark.bm do |x|
x.report("String keys:") do
100000.times { { "name" => "John", "age" => 30 } }
end
x.report("Symbol keys:") do
100000.times { { name: "John", age: 30 } }
end
end2. Freeze String Literals
ruby
# Add magic comment at the top of file
# frozen_string_literal: true
# Or freeze manually
GREETING = "Hello, World!".freeze
# Using string literal optimization
def greet(name)
"Hello, #{name}!" # Creates new string on each call
end
# Optimized version
GREETING_TEMPLATE = "Hello, %s!".freeze
def greet_optimized(name)
GREETING_TEMPLATE % name
end3. Avoid Unnecessary Object Creation
ruby
# Inefficient: Creates temporary array
def sum_squares_bad(numbers)
numbers.map { |n| n * n }.sum
end
# Efficient: Direct calculation
def sum_squares_good(numbers)
numbers.sum { |n| n * n }
end
# Inefficient: String concatenation
def build_string_bad(words)
result = ""
words.each { |word| result += word }
result
end
# Efficient: Using array join
def build_string_good(words)
words.join
end4. Use Object Pool
ruby
class ObjectPool
def initialize(klass, size = 10)
@klass = klass
@pool = Array.new(size) { klass.new }
end
def borrow
@pool.pop || @klass.new
end
def return(obj)
obj.reset if obj.respond_to?(:reset)
@pool.push(obj) if @pool.size < 10
end
end
# Usage example
class ExpensiveObject
def reset
# Reset object state
end
end
pool = ObjectPool.new(ExpensiveObject)
# Borrow object
obj = pool.borrow
# Use object...
pool.return(obj)⚡ Code Optimization Strategies
1. Choose Appropriate Data Structures
ruby
# For lookup operations: Use Set instead of Array
require 'set'
# Inefficient
array = (1..10000).to_a
array.include?(5000) # O(n)
# Efficient
set = (1..10000).to_set
set.include?(5000) # O(1)
# Benchmark test
require 'benchmark'
array = (1..10000).to_a
set = array.to_set
Benchmark.bm do |x|
x.report("Array#include?:") { 1000.times { array.include?(5000) } }
x.report("Set#include?:") { 1000.times { set.include?(5000) } }
end2. Optimize Loops and Iterations
ruby
# Inefficient: Multiple iterations
def process_data_bad(data)
positive = data.select { |x| x > 0 }
squares = positive.map { |x| x * x }
sum = squares.sum
sum
end
# Efficient: Single iteration
def process_data_good(data)
data.sum { |x| x > 0 ? x * x : 0 }
end
# Use each instead of map (when return value is not needed)
# Inefficient
numbers.map { |n| puts n }
# Efficient
numbers.each { |n| puts n }3. Lazy Evaluation and Memoization
ruby
# Using Enumerator::Lazy for large datasets
def process_large_dataset(data)
data.lazy
.select { |item| expensive_condition?(item) }
.map { |item| expensive_transformation(item) }
.first(10) # Only process first 10 matching items
end
# Lazy initialization
class ExpensiveResource
def expensive_data
@expensive_data ||= calculate_expensive_data
end
private
def calculate_expensive_data
# Expensive calculation
sleep(1)
"expensive result"
end
end4. Method Call Optimization
ruby
# Cache method lookups
class OptimizedClass
def initialize
@method_cache = {}
end
def call_method(method_name, *args)
method = @method_cache[method_name] ||= method(method_name)
method.call(*args)
end
end
# Avoid dynamic method calls
# Inefficient
def call_dynamic(obj, method_name)
obj.send(method_name)
end
# Efficient (if possible)
def call_static(obj)
obj.specific_method
end🗄️ Database Query Optimization
1. N+1 Query Problem
ruby
# Problem: N+1 queries
def show_posts_bad
posts = Post.all
posts.each do |post|
puts "#{post.title} by #{post.author.name}" # Queries author for each post
end
end
# Solution: Use includes for eager loading
def show_posts_good
posts = Post.includes(:author)
posts.each do |post|
puts "#{post.title} by #{post.author.name}"
end
end
# Using joins for joined queries
def published_posts_by_active_authors
Post.joins(:author)
.where(published: true, authors: { active: true })
end2. Batch Operations
ruby
# Inefficient: Insert one by one
def create_users_bad(user_data)
user_data.each do |data|
User.create(data)
end
end
# Efficient: Batch insert
def create_users_good(user_data)
User.insert_all(user_data)
end
# Batch update
def update_users_batch(user_ids, attributes)
User.where(id: user_ids).update_all(attributes)
end3. Query Optimization
ruby
# Use select to limit fields
def user_names
User.select(:id, :name) # Only select needed fields
end
# Use limit to limit result count
def recent_posts(limit = 10)
Post.order(created_at: :desc).limit(limit)
end
# Use exists? instead of count > 0
# Inefficient
if Post.where(published: true).count > 0
# ...
end
# Efficient
if Post.where(published: true).exists?
# ...
end🚀 Caching Strategies
1. Memory Cache
ruby
class SimpleCache
def initialize
@cache = {}
end
def get(key)
@cache[key]
end
def set(key, value, ttl = nil)
@cache[key] = {
value: value,
expires_at: ttl ? Time.now + ttl : nil
}
end
def fetch(key, ttl = nil)
cached = @cache[key]
if cached && (cached[:expires_at].nil? || cached[:expires_at] > Time.now)
cached[:value]
else
value = yield
set(key, value, ttl)
value
end
end
end
# Usage example
cache = SimpleCache.new
def expensive_calculation(n)
cache.fetch("calc_#{n}", 300) do # Cache for 5 minutes
# Expensive calculation
(1..n).sum { |i| Math.sqrt(i) }
end
end2. Method-Level Caching
ruby
module Memoizable
def memoize(method_name)
original_method = instance_method(method_name)
define_method(method_name) do |*args|
@_memoized ||= {}
key = [method_name, args]
@_memoized[key] ||= original_method.bind(self).call(*args)
end
end
end
class Calculator
extend Memoizable
def fibonacci(n)
return n if n <= 1
fibonacci(n - 1) + fibonacci(n - 2)
end
memoize :fibonacci
end3. Rails Caching
ruby
# Fragment cache
def show_user_profile(user)
Rails.cache.fetch("user_profile_#{user.id}", expires_in: 1.hour) do
render_user_profile(user)
end
end
# Query cache
def popular_posts
Rails.cache.fetch("popular_posts", expires_in: 30.minutes) do
Post.where("views_count > ?", 1000).order(views_count: :desc).limit(10)
end
end
# Using cache key versioning
def user_data(user)
cache_key = "user_data_#{user.id}_#{user.updated_at.to_i}"
Rails.cache.fetch(cache_key) do
expensive_user_data_calculation(user)
end
end🔄 Concurrency and Asynchronous Processing
1. Thread Pool
ruby
require 'concurrent-ruby'
# Use thread pool for concurrent tasks
pool = Concurrent::ThreadPoolExecutor.new(
min_threads: 2,
max_threads: 10,
max_queue: 100
)
# Submit tasks
futures = (1..100).map do |i|
Concurrent::Future.execute(executor: pool) do
expensive_operation(i)
end
end
# Wait for all tasks to complete
results = futures.map(&:value)2. Asynchronous Processing
ruby
# Using Sidekiq for background job processing
class EmailWorker
include Sidekiq::Worker
def perform(user_id, email_type)
user = User.find(user_id)
EmailService.send_email(user, email_type)
end
end
# Async call
EmailWorker.perform_async(user.id, 'welcome')
# Delayed execution
EmailWorker.perform_in(1.hour, user.id, 'reminder')3. Parallel Processing
ruby
require 'parallel'
# Parallel processing of arrays
results = Parallel.map([1, 2, 3, 4, 5]) do |number|
expensive_calculation(number)
end
# Control number of processes
results = Parallel.map(data, in_processes: 4) do |item|
process_item(item)
end📊 Performance Monitoring
1. Application Performance Monitoring
ruby
class PerformanceMonitor
def self.monitor(operation_name)
start_time = Time.now
memory_before = GC.stat[:total_allocated_objects]
result = yield
end_time = Time.now
memory_after = GC.stat[:total_allocated_objects]
duration = end_time - start_time
memory_used = memory_after - memory_before
puts "#{operation_name}: #{duration}s, #{memory_used} objects allocated"
result
end
end
# Usage example
result = PerformanceMonitor.monitor("Database Query") do
User.includes(:posts).limit(100)
end2. Memory Usage Monitoring
ruby
def memory_usage
`ps -o pid,rss -p #{Process.pid}`.split("\n").last.split.last.to_i
end
def with_memory_monitoring
before = memory_usage
result = yield
after = memory_usage
puts "Memory usage: #{after - before} KB"
result
end
# Usage example
with_memory_monitoring do
large_array = Array.new(1000000) { rand }
end🎯 Performance Optimization Best Practices
1. Measure First
ruby
# Always measure first, then optimize
def optimize_method
# 1. Establish baseline
baseline = Benchmark.measure { original_implementation }
# 2. Implement optimization
optimized_time = Benchmark.measure { optimized_implementation }
# 3. Compare results
improvement = (baseline.real - optimized_time.real) / baseline.real * 100
puts "Performance improved by #{improvement.round(2)}%"
end2. Progressive Optimization
ruby
class DataProcessor
def process(data)
# First version: Simple implementation
# data.map { |item| transform(item) }
# Second version: Batch processing
# process_in_batches(data, 1000)
# Third version: Parallel processing
Parallel.map(data, in_threads: 4) { |item| transform(item) }
end
private
def process_in_batches(data, batch_size)
data.each_slice(batch_size).flat_map do |batch|
batch.map { |item| transform(item) }
end
end
end3. Performance Testing
ruby
# Performance regression testing
RSpec.describe "Performance" do
it "processes 1000 items within 1 second" do
data = Array.new(1000) { rand }
expect {
DataProcessor.new.process(data)
}.to perform_under(1.second)
end
it "uses less than 100MB memory" do
expect {
large_operation
}.to allocate_under(100.megabytes)
end
end🔧 Ruby-Specific Optimization Techniques
1. String Operation Optimization
ruby
# Use String#<< instead of String#+
def build_string_efficient(parts)
result = String.new
parts.each { |part| result << part }
result
end
# Use StringIO for large string operations
require 'stringio'
def build_complex_string(data)
io = StringIO.new
data.each { |item| io << process_item(item) }
io.string
end2. Array Operation Optimization
ruby
# Pre-allocate array size
def create_large_array(size)
Array.new(size) # More efficient than [] then multiple <<
end
# Use flat_map instead of map + flatten
# Inefficient
result = array.map { |item| process_item(item) }.flatten
# Efficient
result = array.flat_map { |item| process_item(item) }3. Hash Operation Optimization
ruby
# Use Hash#fetch to set default values
def count_items(items)
counts = Hash.new(0) # Set default value
items.each { |item| counts[item] += 1 }
counts
end
# Use transform_values for hash value conversion
hash.transform_values { |value| value.upcase }📈 Performance Optimization Checklist
- [ ] Use performance analysis tools to identify bottlenecks
- [ ] Optimize database queries (avoid N+1 problem)
- [ ] Implement appropriate caching strategies
- [ ] Choose appropriate data structures
- [ ] Avoid unnecessary object creation
- [ ] Use batch operations for large data
- [ ] Consider async processing for time-consuming operations
- [ ] Monitor memory usage
- [ ] Write performance tests to prevent regression
- [ ] Regularly review and optimize critical code paths
Remember, premature optimization is the root of all evil. Always ensure code correctness first, then measure to find real performance bottlenecks, and finally optimize targetedly. Ruby's philosophy is developer happiness and productivity. Don't forget to maintain code readability and maintainability while pursuing performance.