ruby 撰寫上該注意的效能細節

ruby 提供非常多的函式,但同一種需求有不同寫法,往往我們都沒注意到效能細節,只知道要把功能寫出來。最近 github 上便有人針對類似功能的函式作些效能比較,在此針對特別或是速度差很多的地方作些個人筆記。原出處:https://github.com/JuanitoFatas/fast-ruby

註:函式名稱會有標記哪種寫法較快,是 ruby 2.2.0 版本

多個變數的 assignment

def fast
  _a, _b, _c, _d, _e, _f, _g, _h = 1, 2, 3, 4, 5, 6, 7, 8
  nil
end

def slow
  _a = 1
  _b = 2
  _c = 3
  _d = 4
  _e = 5
  _f = 6
  _g = 7
  _h = 8
  nil
end

Parallel Assignment 比 Sequential Assignment 快上 1.22 倍

Array#bsearch & Array#find

data = [*0..100_000_000]

Benchmark.ips do |x|
  x.report('find')    { data.find    { |number| number > 77_777_777 } }
  x.report('bsearch') { data.bsearch { |number| number > 77_777_777 } }
  x.compare!
end

find 與 bsearch 主要是找到 array 裡面第一個符合條件的元素,在大筆數據上,bsearch 竟然比 find 快上 3137489.63 倍!

Array#length & Array#size & Array#count

ARRAY = [*1..100]

Benchmark.ips do |x|
  x.report("Array#length") { ARRAY.length }
  x.report("Array#size") { ARRAY.size }
  x.report("Array#count") { ARRAY.count }
  x.compare!
end
  • length 單純回傳 array 裡面有幾個 element
  • size 是 length 的別名(一樣的事情,只是不同名字)
  • count 會實際去計算裡面有幾個 element
    length = size 比 count 快上 1.24 倍(length 比 size 快上 1.01 倍,大概就是別名轉換的時間吧!)

Array#shuffle.first & Array#sample

sample 跟 shuffle.first 一樣都是做「取得隨機一個陣列中的元素」,但 sample 比 shuffle.first 快上 18.82 倍!(而且只在 array 只有 100 個元素下)

Array[] & Array#first & Array#last

Define: ARRAY = [*1..100]

  • ARRAY[0] 比 ARRAY.first 快上 1.15 倍
  • ARRAY[-1] 比 ARRAY.last 快上 1.12 倍

Enumerable#each_with_index & while

ARRAY = [*1..100]

def slow
  ARRAY.each_with_index do |number, index|
    number + index
  end
end

def fast
  index = 0
  while index < ARRAY.size
    ARRAY[index] + index
    index += 1
  end
end

Benchmark.ips do |x|
  x.report('each_with_index') { slow  }
  x.report('While Loop')      { fast  }
  x.compare!
end

這結果蠻令人吃驚,單純用 while 去做 index 比 each_with_index 快上 1.88 倍。

Enumerable#map & Array#flatten & Enumerable#flat_map

ARRAY = (1..100).to_a

def slow_flatten_1
  ARRAY.map { |e| [e, e] }.flatten(1)
end

def slow_flatten
  ARRAY.map { |e| [e, e] }.flatten
end

def fast
  ARRAY.flat_map { |e| [e, e] }
end

flat_map 寫法快上 1.6x 倍!

Hash#merge & Hash#merge!

ENUM = (1..100)

def slow
  ENUM.inject({}) do |h, e|
    h.merge(e => e)
  end
end

def fast
  ENUM.inject({}) do |h, e|
    h.merge!(e => e)
  end
end

原來 merge! (有加驚嘆號)比 merge 快上整整 24 倍!(兩者差異在,沒加驚嘆號會是回傳一個新的 Array,有加驚嘆號是直接套用在呼叫的 Array 上)

cover? & include?

BEGIN_OF_JULY = Date.new(2015, 7, 1)
END_OF_JULY = Date.new(2015, 7, 31)
DAY_IN_JULY = Date.new(2015, 7, 15)

def fast
  (BEGIN_OF_JULY..END_OF_JULY).cover? DAY_IN_JULY
end

def slow
  (BEGIN_OF_JULY..END_OF_JULY).include? DAY_IN_JULY
end

range 內找是否有這個 element 的情況下,cover? 比 include? 快上 23.35 倍!