Skip to content
AgileLogics
6 min read

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

  1. Database queries — N+1 queries (one query per item in a loop) and missing indexes dwarf everything else in web apps.
  2. Network calls — cache results, make calls concurrently, set timeouts.
  3. Algorithms — a Hash or Set lookup instead of searching an array inside a loop turns O(n²) into O(n).
  4. Allocations in hot loops — see the previous lesson.
  5. YJIT — often a free improvement.

Common mistakes

  • Micro-optimizing Ruby syntax (each vs for) while the page makes 200 database queries.
  • Benchmarking with tiny inputs, where fixed overhead dominates.