如何寫出有效率的 Ruby Code

又來推薦 PDF 武功秘笈, Writing Efficient Ruby Code

Ruby 是個很慢的語言,但有些作法應用得當,還是會有不錯的改善。不過要知道程式碼的可讀性跟執行效率有時候是衝突的,這點還需拿捏,尤其 software life cycle 一開始可讀性比較重要。有句最佳化的經典名言一定要引一下:

未成年就這麼優,是一切邪惡的根源
Premature optimization is the root of all evil

這裡紀錄一些看到有趣的事情,PDF 裡有更詳盡的 example code。

Instance Variables versus Accessors

@attrubute 比 self.attrbute 快(method call 比較貴),如果你不需要 public method 或有 sub-class 的需求,請考慮不要用 attr_accessor 等方式來建立 read/write method。

Local Variables Are Cheap

method 中傳進來的參數若常用,可以先存成 Local Variables 再來多次使用。

Interpolated Strnges

方法一 s = “:#{a}.#{b}” 比方法二 s = “:” << a.to_s << “.” << b.to_s 快多囉!! 不需要多用 << method call。另外單引號跟雙引號沒有效率上的差別。

In-Place Updates

直接修改比複製一份快: gsub! 比 gsub 快,merge! 比 merge 快。例如這個範例 s.gsub().gsub!().gsub! 而不是 s.gsub.gsub.gsub

不過要小心,Ruby 對於 ! 版本的方法回傳值有時候不是自己(得查 API 確定一下),因此沒辦法用串接寫法。

Set Versus Array

如果你只用到 Array 的 uniq, |, %, – 等群集操作,請考慮改用 Set 會比較快,像是 include? 就可以在 O(1) 做完。而 Hash 又比 Array 貴。

Make Decisions at Load Time

注意到在 module 或 class 的 definition scope 也是可以執行程式的,而且只有第一次 require source code 時會執行編譯一次。

class Foo
if C=’somesetting’
def A
…version 1
end
else
def A
…version 2
end
end
end

Self-Modifying Code

避免寫出自己修改自己定義的程式(也不好讀code吧),改用 singleton 方式來做 alias_method 跟 remove_method。

Test Most Frequent Case First

用 case 時把較常發生的放前面,若都差不多,把貴的操作的放後面。

Optimize Access to Global Constants

在 Constant 前面加上 namespace operator :: 會比較快,減少查詢時間

Caching Data in Instance Variables

例如在 controller 裡面 def captial_letters { (“A”..”Z”).to_a } end 請改用 def captial_letters { @captial_letters ||= (“A”..”Z”).to_a } end

Caching Data in Class Variables

承上,如果 data 較大又持續存在,可以改用 @@capital_letter = (“A”..”Z”).to_a 然後定 def captial_letters { @@captial_letters } end。在 ActiveRecord 中也可以先把 DB 的資料讀出來當作 CONSTANTS,就不需要每次都查資料庫了:

class State < ActiveRecord::Base
NAMES_ABBR = self.find(:all).map do { |s| [s.name,s.abbr] }
end

Coding Variable Caching Efficiently

愛用 @var ||= begin …expr… end 而不是 @var = begin …expr… end unless @var,這樣只需查 @var 一次。但如果 @var要能吃 nil 或 false 就只能多一個 boolean 變數來幫忙了。

Initializing Variables with nil

若沒有給值,就不需要初始成nil

Using .nil?

如果要測試的值已知不是 Boolean,那就不需要用 .nil 多一個 method call,直接 if x 就好了而不是 if x.nil?

nil? or empty? versus blank?

ActionPack 有提供 x.blank?,不需要用 x.nil? || x.empty?

Using return

雖然 Ruby 很聰明會自動回傳最後運算的值,但明確寫 return 會比較快 (Ruby 1.9有改善)

Using any?

用 empty? 來測試 string, array or hash 而不要用 any?,速度差個兩倍 (而且 Ruby 1.9 也拿掉 any?這個method了)

Block Local Variables

在 Ruby 1.8 裡面,在 Block 裡面存取 Block 外面的 local variable 竟然比裡面的 local variable 還快!! 不過 Ruby 1.9 修正這個奇怪的事實就是了。

Parallel Assignments

很方便沒錯,但 a,b=1,2 運算還丟出來 Array [1,2] 是垃圾。(Ruby 1.9 修正了,改丟 true)

Date Formatting

用 parse_date 把 String 變成 Date 是個非常昂貴的操作,建議你直接用字串操作(正規表示法)把 String(例如從DB撈出來的db_date) 變成你想要的格式,或是從中拆出你想要的年月日。(PDF 有 example code)

Temporary Data Structure Constants

如果用到一個暫時的資料結構(如Array)但接下來不會更改,可以改用 Constant 宣告,並避免複製。

ObjectSpace.each_object

DO NOT USE IT,你不會想在 per-request 下去執行的,很貴。

Unnecessary Block Parameters

def foo(bar, &block) 的 &block 若在函式內不需要用到(可以直接用 yield 使用的),請不要加。轉成 Proc 比單純 yield 的慢5倍!!

Symbol.to_proc

ActiveSupport 提供的 @var.map(&:name) 語法,雖然方便,但是用 inline block 的方法 @var.map{ |a| a.name| } 執行效率比較快。

Requiring Files Dynamically

在 Rails 中若碰到一個未定義的常數,它會自動去 load source files 來載入定義。為了避免混淆 file-loading 機制,不需寫 require ‘client’ (這又是另一套 auto-loaded 機制),只要單單一行寫 Client 就好了。

Including Modules versus Opening Classes

使用 module mix-in 會比打開 class 加入 method 還慢一些,如果你的 module 只針對一個 class 做 mix-in,那不如直接打開來加入 methods 就好了。

參與討論

8 則留言

  1. 自動引用通知: Tuo_Huang » Rails最佳實踐
  2. SET VERSUS ARRAY
    如果你只用到 Array 的 uniq, |, %, – 等群集操作,請考慮改用 Set 會比較快,像是 include? 就可以在 O(1) 做完。
    这里的再O(1)做完没有看懂@_@,可以简单说下吗谢谢

  3. 演算法的 time-complexity (aka “Big O” or “Big Oh”)。O(1) 意指無論數量多大,都是一個常數時間內完成,O(n) 則表示和數量成線性。

    查詢一個元素是否在 Set 裡是 O(1) 的演算法,換成用 Array 的話,變成 O(n)。

發佈留言

發表迴響