awesomeprogrammer.com

Sharing ideas

Solving problems

Gathering solutions

Exchanging thoughts

Ruby On Rails

PHP

Postgres

Debian & Ubuntu
jQuery & CSS

Why My Code Is So Darn Slow?!

Slow code?

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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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:

1
2
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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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

Comments