-> <-
Ruby is very pretty, and unfortunattely it can be very slow. Beautiful syntax and flexebility of this language has its price (well, it’s not so bad compared to other interpreted languages anyway).
Let’s go over few (totally random) things that might cause performance issues.
Abusing rails view helpers
Rails helpers are very useful, but if you’re calling various helpers hundreds of times on a single page you might be surprised how much overhead it might produce. Do you really need that number_with_delimiter
everywhere? Maybe you can use something simpler than number_with_precision
?
Take a look at those two benchmarks:
require "action_view" #rails 4.1.4
require "benchmark"
class Helper
extend ActionView::Helpers::NumberHelper
end
iterations = (1..100_000)
Benchmark.bm(20) do |x|
x.report("number_with_delimiter") { iterations.each { |i| Helper.number_with_delimiter(100) } }
x.report("without helper") { iterations.each { |i| i } }
end
Benchmark.bm(20) do |x|
x.report("number_with_precision") { iterations.each { |i| Helper.number_with_precision(i * 1.1123, precision: 2) } }
x.report("without helper") { iterations.each { |i| '%.2f' % (i*1.1123) } }
end
# user system total real
# number_with_delimiter 3.350000 0.010000 3.360000 ( 3.360141)
# without helper 0.000000 0.000000 0.000000 ( 0.004517)
# user system total real
# number_with_precision 10.220000 0.010000 10.230000 ( 10.221791)
# without helper 0.120000 0.000000 0.120000 ( 0.121396)
If you’re generating huge lists, summaries or reports - every ms matters.
Hashes - to .fetch or not to .fetch?
Stop and thing about very simple thing as retrieving value from a hash. Let’s run some benchmarks:
require "benchmark"
require "ostruct"
iterations = (1..100_000)
hash = {}
iterations.each { |i| hash[i] = i }
Benchmark.bm(20) do |x|
x.report("fetch, proc") { iterations.each { |i| hash.fetch(i) { OpenStruct.new(a: "one", b: "two") } } }
x.report("fetch, argument") { iterations.each { |i| hash.fetch(i, OpenStruct.new(a: "one", b: "two"))} }
x.report("[]") { iterations.each { |i| hash[i] } }
end
# user system total real
# fetch, proc 0.020000 0.000000 0.020000 ( 0.024753)
# fetch, argument 1.080000 0.010000 1.090000 ( 1.085491)
# [] 0.020000 0.000000 0.020000 ( 0.019607)
[]
seems like the fastest way (by tiny margin) to get the value, but what the hell happened in fetch
with provided default argument?! Let me tell you that - it’s a trap ;-). If you provide argument to a fetch method it will be always evaluated, proc
on the other hand is only called when given key is missing.
Another gotcha related to hashes that is worth mentioning here:
puts({ "false" => false}["false"] || true) # -> true
puts({ "false" => false }.fetch("false") { true }) # -> false
Use Ruby’s native methods
Even simple things like checking if given date is a weekend can be optimized. In example below first method uses naive approach, when second method refers to methods provided by Date
class.
require "benchmark"
iterations = (1..100_000)
days = {}
iterations.each { |i| days[i] = Time.at(i * 3600) }
class Weekend
def self.include?(date)
[0,6].include?(date.wday)
end
def self.ruby(date)
date.sunday? || date.saturday?
end
end
Benchmark.bm(20) do |x|
x.report("check array") { iterations.each { |i| Weekend.include?(days[i]) } }
x.report("use c method") { iterations.each { |i| Weekend.ruby(days[i]) } }
end
# user system total real
# check array 0.160000 0.020000 0.180000 ( 0.184166)
# use c method 0.040000 0.000000 0.040000 ( 0.039794)
Memoize gotcha
Often you want to cache some method’s result into instance variable (simply by using ||=
), but let’s say it’s a complex SQL query/algorithm that returns false
- if this is the case it will be executed over and over again, you might even not notice it at the beginning. The solution to this problem is quite simple, but worth mentioning.
require "benchmark"
require "ostruct"
class VariableCache
def method1
@method1 ||= begin
OpenStruct.new(a: "b")
false
end
end
def method2
@method2 = begin
OpenStruct.new(a: "b")
false
end unless defined?(@method2)
@method2
end
end
iterations = (1..100_000)
cache_test = VariableCache.new
Benchmark.bm(20) do |x|
x.report("method1") { iterations.each { cache_test.method1 }}
x.report("method2") { iterations.each { cache_test.method2 }}
end
# user system total real
# method1 0.620000 0.000000 0.620000 ( 0.629754)
# method2 0.010000 0.000000 0.010000 ( 0.013680)
Those tiny things can add up and affect usability of your app. If you ever encounter performance issues stop for a minute, browse your code, benchmark it. You can help yourself by using ruby-prof or rack-mini-profiler - those two tools are quite powerful, but that’s a topic for yet another blog post ;).
Note: Benchmarks executed on ruby 2.1.2p95