Ruby Performance Optimization — Profiling Caching YJIT and Memory Management Explained
In this tutorial, you will learn about Ruby Performance Optimization. We cover key concepts, practical examples, and best practices to help you master this topic.
Ruby Performance Optimization covers profiling with benchmark, stackprof, and rack-mini-profiler, Caching strategies with memoization and Redis, YJIT JIT Compilation, and memory management techniques.
What You'll Learn
- Profiling Ruby code
- Caching strategies
- YJIT JIT compilation
- Memory and GC optimization
Why It Matters
Performance affects user experience. Doda Browser optimizes page load times. Durga Antivirus Pro optimizes scan throughput. One well-placed cache or optimized query can 10x your application speed.
Real-World Use
Rails applications use fragment caching, Russian Doll caching, and query caching. Background Jobs optimize batch processing. APIs use HTTP caching. Web servers use connection pooling.
flowchart LR
A["Performance"] --> B["Profiling"]
B --> C["Caching"]
C --> D["YJIT"]
D --> E["GC Tuning"]
A:::current --> B
style A fill:#2563eb,stroke:#2563eb,color:#fff
style B fill:#dbeafe,stroke:#2563eb,color:#1e40af
style C fill:#dbeafe,stroke:#2563eb,color:#1e40af
style D fill:#dbeafe,stroke:#2563eb,color:#1e40af
style E fill:#f1f5f9,stroke:#94a3b8,color:#64748b
Profiling with Benchmark
require "benchmark"
n = 1_000_000
Benchmark.bm do |x|
x.report("string+") { n.times { "foo" + "bar" } }
x.report("interpol") { n.times { "foo#{'bar'}" } }
x.report("concat") { n.times { "foo".concat("bar") } }
end
# user system total real
# string+ 0.123456 0.000123 0.123579 (0.123789)
# interpol 0.098765 0.000098 0.098863 (0.098912)
# concat 0.087654 0.000087 0.087741 (0.087823)
Benchmark.bmbm
# Rehearsal to stabilize performance
Benchmark.bmbm do |x|
x.report("fast") { fast_method }
x.report("slow") { slow_method }
end
Profiling with StackProf
require "stackprof"
StackProf.run(mode: :cpu, out: "tmp/stackprof.dump") do
# Code to profile
1000.times { expensive_operation }
end
# Analyze with:
# stackprof tmp/stackprof.dump
# stackprof tmp/stackprof.dump --method expensive_method
Memory Profiling
require "memory_profiler"
report = MemoryProfiler.report do
# Code to profile
array = 1000.times.map { |i| "string #{i}" }
end
report.pretty_print
# Shows: allocated memory, retained memory, objects by location
Caching Strategies
Memoization
class ExpensiveService
def compute(id)
@cache ||= {}
@cache[id] ||= begin
puts "Computing for #{id}..."
sleep(1) # Simulate expensive operation
id * 2
end
end
end
service = ExpensiveService.new
puts service.compute(5) # Computing for 5... => 10
puts service.compute(5) # => 10 (cached, no computation)
Redis Caching
require "redis"
class CacheStore
def initialize
@redis = Redis.new(url: ENV["REDIS_URL"])
end
def fetch(key, ttl: 300)
cached = @redis.get(key)
return JSON.parse(cached) if cached
value = yield
@redis.setex(key, ttl, value.to_json)
value
end
def delete(key)
@redis.del(key)
end
def clear_pattern(pattern)
keys = @redis.keys(pattern)
@redis.del(*keys) unless keys.empty?
end
end
cache = CacheStore.new
result = cache.fetch("user:#{user_id}", ttl: 60) do
User.find(user_id).expensive_query
end
Rails Caching
# Fragment cache
<% cache @article do %>
<%= render @article %>
<% end %>
# Russian doll caching
<% cache [@article, @article.comments.maximum(:updated_at)] do %>
<%= render @article %>
<%= render @article.comments %>
<% end %>
# Low-level caching
Rails.cache.fetch("expensive_calc", expires_in: 1.hour) do
expensive_calculation
end
# HTTP caching
class ArticlesController
def show
@article = Article.find(params[:id])
fresh_when @article
end
end
YJIT Optimization
# Enable YJIT (Ruby 3.1+)
# Add to your application:
# RUBYOPT="--yjit"
# Or in config:
# $ ruby --yjit myapp.rb
# YJIT stats
RubyVM::YJIT.runtime_stats # Returns hash of JIT stats
# Example YJIT configuration
# --yjit-exec-mem=256 # Code cache size
# --yjit-call-threshold=30 # Calls before JIT compilation
Object Allocation Reduction
# Bad — creates many intermediate objects
def process(items)
items.map { |i| i.to_s }
.select { |s| s.length > 3 }
.map(&:upcase)
end
# Better — single pass
def process(items)
result = []
items.each do |i|
s = i.to_s
result << s.upcase if s.length > 3
end
result
end
# Avoid creating unnecessary objects
# Bad: "Hello, #{name}!" # Creates new string
# Good: "Hello, #{name}!" # Same — Ruby handles this efficiently
GC Tuning
# Check current GC settings
GC.stat
GC.latest_gc_info
# Tune GC
GC::Profiler.enable
# Run code...
GC::Profiler.report
# Major GC tuning
# RUBY_GC_HEAP_GROWTH_FACTOR=1.15
# RUBY_GC_HEAP_OLDOBJECT_LIMIT_FACTOR=2.0
# RUBY_GC_MALLOC_LIMIT=67108864
# Manual GC
GC.start # Force GC
GC.compact # Compact heap (Ruby 2.7+)
Common Mistakes
1. Premature Optimization
# Bad — optimizing before measuring
def process(x)
# Complex optimization
cache[x] ||= begin
result = x * 2
result -= 1 if result > 100
result
end
end
# Good — measure first, optimize bottlenecks
2. Over-Caching
# Cache stampede when cache expires
# Multiple requests all compute the same value
# Solution: use Rails.cache.fetch with race_condition_ttl
Rails.cache.fetch("key", expires_in: 1.hour, race_condition_ttl: 10) do
expensive_calculation
end
3. Not Using Indexes
# Bad — full table scan
User.where("name LIKE '%search%'")
# Good — use database indexes
add_index :users, :name
4. N+1 Queries
# Bad — queries for each article's comments
@articles.each { |a| a.comments.count }
# Good — use includes or counter_cache
@articles = Article.includes(:comments)
5. Creating Unnecessary Objects in Loops
# Bad — creates new array each iteration
items.each do |i|
[i, i * 2, i * 3].each { |v| process(v) }
end
# Good — reuse
values = [nil, nil, nil]
items.each do |i|
values[0] = i
values[1] = i * 2
values[2] = i * 3
values.each { |v| process(v) }
end
Practice Questions
1. What is YJIT and how does it help?
YJIT (Yet Another Ruby JIT) compiles hot code paths to native machine code at runtime, improving execution speed by 2-5x for CPU-bound operations. Enable with --yjit.
2. What is memoization?
Caching the result of a method call so subsequent calls return the cached value without re-computing. Pattern: @cache ||= compute_value.
3. How do you find performance bottlenecks?
Use Benchmark for micro-benchmarks, StackProf for CPU profiling, rack-mini-profiler for Rails request profiling, and MemoryProfiler for memory analysis.
4. What is Russian doll caching?
Nested cache keys based on record timestamps. When a child record updates, only its parent's cache is invalidated. cache [article, article.comments.maximum(:updated_at)].
Challenge: Optimize a method that processes a CSV of user data, using profiling to identify and fix bottlenecks.
Solution
require "csv"
require "benchmark"
# Before optimization
def process_csv_slow(filepath)
users = []
CSV.foreach(filepath, headers: true) do |row|
users << {
name: row["name"].strip,
email: row["email"].strip.downcase,
age: row["age"].to_i
}
end
users.select { |u| u[:age] >= 18 }
.sort_by { |u| u[:name] }
end
# After optimization
def process_csv_fast(filepath)
users = []
CSV.foreach(filepath, headers: true) do |row|
name = row["name"]
email = row["email"]
age = row["age"]
next unless name && email && age
name.strip!
email = email.strip.downcase
age = age.to_i
users << { name: name, email: email, age: age } if age >= 18
end
users.sort_by! { |u| u[:name] }
users
end
Benchmark.bm do |x|
x.report("slow") { process_csv_slow("test.csv") }
x.report("fast") { process_csv_fast("test.csv") }
end
FAQ
{{< faq question="Should I always enable YJIT?" >}}
Yes for Ruby 3.3+. YJIT is production-ready and provides 2-5x speedup for most workloads. Enable with RUBYOPT="--yjit" or ruby --yjit your_app.rb.
{{< /faq >}}
{{< faq question="How often should I profile my application?" >}} After every significant code change. Monitor production performance with APM tools (Scout, New Relic). Profile whenever you add a new feature that could be performance-sensitive. {{< /faq >}}
{{< faq question="What's the most impactful performance optimization?" >} Database query optimization (N+1, missing indexes). Then caching. Then Ruby-level optimizations (allocation, GC). Measure before optimizing. {{< /faq >}}
{{< faq question="How does Ruby's garbage collector work?" >}}
Ruby uses a generational GC (since 2.1) with minor GC for young objects and major GC for old objects. GC.stat shows collection counts and timings.
{{< /faq >}}
{{< faq question="Should I freeze strings for performance?" >}}
frozen_string_literal: true (Ruby 3.0+) makes all string literals frozen by default, reducing allocations. Add it at the top of files: # frozen_string_literal: true.
{{< /faq >}}
Try It Yourself
# performance_demo.rb
require "benchmark"
def slow_method(items)
result = []
items.each do |i|
result << i.to_s.upcase + "!"
end
result
end
def fast_method(items)
items.map! { |i| "#{i.to_s.upcase}!" }
end
data = (1..10000).to_a
Benchmark.bm do |x|
x.report("slow") { slow_method(data) }
x.report("fast") { fast_method(data.dup) }
end
# Compare memory
require "memory_profiler"
report = MemoryProfiler.report do
fast_method((1..1000).to_a)
end
report.pretty_print
What's Next
Now that you understand performance optimization, learn about threading and concurrency in Ruby.
| Topic | Description | Link |
|---|---|---|
| Ruby Threads | Thread creation, synchronization | {{< ref "40-threads" >}} |
| Ruby Ractors | Parallel execution, Ractors | {{< ref "42-ractors" >}} |
| Python Profiling | Compare Python's profiling tools | Python |
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro