Performance and Profiling
Prerequisites: Memory and garbage collection
The first rule of performance work: measure before you change anything. Guesses about what's slow are wrong surprisingly often.
Benchmark — compare alternatives
require "benchmark"
words = %w[ruby rails rack puma] * 50_000
Benchmark.bm(10) do |x|
x.report("map+join") { words.map(&:upcase).join(",") }
x.report("buffer") { buf = +""; words.each { |w| buf << w.upcase << "," } }
end
user system total real
map+join 0.020000 0.000000 0.020000 ( 0.021893)
buffer 0.031000 0.000000 0.031000 ( 0.030450)
(Your numbers will differ.) The real column is wall-clock time. Run
each benchmark more than once — the first run includes warm-up. The
benchmark-ips gem gives more reliable "iterations per second"
comparisons. (benchmark ships with Ruby 3.4; newer versions make it a
bundled gem, so list gem "benchmark" in a Bundler project.)
Profiling — find where time goes
A profiler samples a running program and reports which methods it spent its time in. For Ruby, stackprof and vernier are popular:
require "stackprof"
StackProf.run(mode: :wall, out: "tmp/profile.dump") do
generate_report
end
$ bundle exec stackprof tmp/profile.dump --text --limit 3
TOTAL (pct) SAMPLES (pct) FRAME
412 (68.2%) 301 (49.8%) Report#format_rows
97 (16.1%) 97 (16.1%) JSON::Ext::Generator...
Fix the top item, measure again, and stop when it's fast enough.
YJIT — Ruby's built-in JIT compiler
YJIT compiles frequently-run Ruby code to machine code while the program runs. It's included in CRuby (3.1+) and typically speeds up real applications noticeably, at the cost of some extra memory. Turn it on with a flag or an environment variable:
ruby --yjit app.rb
RUBY_YJIT_ENABLE=1 bundle exec puma
Check it's active with RubyVM::YJIT.enabled?. Rails 7.2 and later
enable YJIT automatically when it's available.
Common wins, in order of how often they matter
- Database queries — N+1 queries (one query per item in a loop) and missing indexes dwarf everything else in web apps.
- Network calls — cache results, make calls concurrently, set timeouts.
- Algorithms — a
HashorSetlookup instead of searching an array inside a loop turns O(n²) into O(n). - Allocations in hot loops — see the previous lesson.
- YJIT — often a free improvement.
Common mistakes
- Micro-optimizing Ruby syntax (
eachvsfor) while the page makes 200 database queries. - Benchmarking with tiny inputs, where fixed overhead dominates.