這篇文章主要介紹了我是如何把ruby gem contracts.ruby速度提升10倍的。
contracts.ruby在我項目里用來添加代碼合約(code contracts)到Ruby中??雌饋聿畈欢嗍沁@樣的:
Contract Num, Num => Num
def add(a, b)
a + b
end
只要add方法被調用,參數和返回值都會被檢查。
20秒
本周末,我對該庫進行了測試,發(fā)現(xiàn)其性能非常糟:

這是在隨機輸入下,運行1000次以后的結果。
所以,當給一個函數加入合約功能后,運行速度明顯下降(約40倍這樣),對此,我進行了深入的研究。
8秒
我取得了較大的進展,當傳遞合約時,我調用success_callback函數,該函數是個空函數,下面是這個函數的整個定義:
def self.success_callback(data)
end
原來函數調用在Ruby中是非常昂貴的,僅刪除這個調用,就節(jié)省了8秒鐘:
刪除其它一些附件函數的調用,時間花費開始從9.84-> 9.59-> 8.01秒,該庫的速度馬上提升到以前的兩倍了。
現(xiàn)在,事情變的有點復雜了。
5.93秒
這里有許多年種定義一個合約的方式:匿名(lambdas)、類 (classes)、簡單舊數據(plain ol' values)等。 我有個很長的case語句,用來檢測合約的類型。在此合約類型基礎之上,我可以做不同的事情。通過把它改為if語句,我節(jié)約了一些時間,但每次調用這個函數時,我仍然耗費了不必要的時間在仔細檢查這個判定樹上面:
if contract.is_a?(Class)
# check arg
elsif contract.is_a?(Hash)
# check arg
...
當定義合約和構建lambda時,對樹只做一次檢查:
if contract.is_a?(Class)
lambda { |arg| # check arg }
elsif contract.is_a?(Hash)
lambda { |arg| # check arg }
然后,我將完全繞過邏輯分支,通過將參數傳遞給預計算的lambda來進行驗證,這樣就節(jié)約了1.2秒時間。

預計算一些其它的If語句,差不多又節(jié)省了1秒時間:

5.09秒
將.zip轉換為.times又為我節(jié)省了1秒時間:

結果證明:
args.zip(contracts).each do |arg, contract|
上面的代碼要比下面這個慢:
args.each_with_index do |arg, i|
要比下面這個更慢:
.zip要花費不必要的時間復制和創(chuàng)建新的數組。而我認為,.each_with_index之所以慢,是因為它受制于背后的.each,所以它涉及到兩個限制而不是一個。
4.23秒
下面再看些細節(jié)的東西,contracts庫在工作時,它會為每一個方法添加class_eval(class_eval要比define_method快)的新方法,這個新方法里有一個對老方法的引用,當調用新方法時,它會檢查參數,然后根據參數調用老方法,然后再檢查返回值,并且返回值。所有這些都會調用Contract class的check_args和check_result兩個方法。我取消了這兩個方法的調用,并且對新方法進行正確檢查,結果又節(jié)省了0.9秒:

2.94秒
在上面,我已經解釋了如何基于Contract類型創(chuàng)建lambda,然后使用這些來檢驗參數。現(xiàn)在,我換了種方法,用生成代碼來替代,當我使用class_eval創(chuàng)建新方法時,它就會從eval中獲得結果。一個可怕的漏洞,但它避免了一大堆方法調用,并且節(jié)省了1.25秒:

1.57秒
最后,我改變了調用重寫方法的方式,我先前是使用引用:
# simplification
old_method = method(name)= method(name)
class_eval %{%{
def #{name}(*args)def #{name}(*args)
old_method.bind(self).call(*args).bind(self).call(*args)
endend
}}
我進行了修改,并使用alias_method方法:
alias_method :"original_#{name}", name:"original_#{name}", name
class_eval %{%{
def #{name}(*args)def #{name}(*args)
self.send(:"original_#{name}", *args)self.send(:"original_#{name}", *args)
endend
}}
驚喜,又節(jié)省了1.4秒。我不知道為什么aliaa_method會如此地快,我猜是因為它跳過了一個方法的調用和綁定到.bindbind。

結果
我們成功的將時間從20秒優(yōu)化到1.5秒,我不認為還有比這更好的結果的了。我所編寫的 這個測試腳本表明,一個被封裝過的add方法要比常規(guī)的add方法慢3倍,所以這些數字已經足夠好了。
想要驗證上面的結論很簡單,大量的時間花在調用方法上是只慢3倍的原因,這里有個更現(xiàn)實的例子:一個函數讀一個文件100000次:

稍微慢了點!add函數是個例外,我決定不再使用alias_method方法,因為它污染了命名空間,并且這些別名函數會到處出現(xiàn)(文檔、IDE的自動完成等)。
其它原因:
在Ruby中調用方法很慢,我喜歡將代碼模塊化和重復使用,但或許是時候將更多的代碼進行內聯(lián)了。
測試你的代碼!刪掉一個簡單的未使用的方法時間從20秒縮短到了12秒。
其它嘗試
1.方法選擇器
Ruby 2.0里缺少方法選擇器這一特性,否則你還可以這樣寫:
class Foo Foo
def bar:beforedef bar:before
# will always run before bar, when bar is called# will always run before bar, when bar is called
endend
def bar:afterdef bar:after
# will always run after bar, when bar is called# will always run after bar, when bar is called
# may or may not be able to access and/or change bar's return value# may or may not be able to access and/or change bar's return value
endend
endend
這樣可能會更加容易編寫decorator,并且運行速度也會加快。
2.關鍵字old
Ruby 2.0里缺乏的另一特性是引用重寫方法:
class Foo Foo
def bardef bar
'Hello''Hello'
endend
end end
class Fooclass Foo
def bardef bar
old + ' World'+ ' World'
endend
endend
Foo.new.bar # => 'Hello World'Foo.new.bar # => 'Hello World'
3.使用redef重新定義方法:
Matz曾說過:
為了消除alias_method_chain,我們引入了Module#prepend,prepend前面加#號,這樣就沒機會在語言里加入冗余特性。
所以如果redef是冗余特征,也許prepend可以用來寫decorator?
4.其它實現(xiàn)
目前為止,這些都已經在YARV做過測試。
您可能感興趣的文章:- 詳解Ruby中的異常
- 在Ruby中處理文件的輸入和輸出的教程
- Ruby中操作文件的方法介紹