本文介紹了如何優化Ruby代碼以提高程序的運行速度,本文中引用的項目將Ruby項目的運行時間從20秒優化為1.5秒,需要它的朋友可以參考武林技術頻道小編為大家介紹的內容,一起來看看吧!
contracts.ruby在我項目里用來添加代碼合約(code contracts)到Ruby中??雌饋聿畈欢嗍沁@樣的:
Contract Num, Num => Numdef add(a, b) a + bend
只要add方法被調用,參數和返回值都會被檢查。
20秒
本周末,我對該庫進行了測試,發現其性能非常糟:
這是在隨機輸入下,運行1000次以后的結果。
所以,當給一個函數加入合約功能后,運行速度明顯下降(約40倍這樣),對此,我進行了深入的研究。
8秒
我取得了較大的進展,當傳遞合約時,我調用success_callback函數,該函數是個空函數,下面是這個函數的整個定義:
def self.success_callback(data)end
原來函數調用在Ruby中是非常昂貴的,僅刪除這個調用,就節省了8秒鐘:
刪除其它一些附件函數的調用,時間花費開始從9.84-> 9.59-> 8.01秒,該庫的速度馬上提升到以前的兩倍了。
現在,事情變的有點復雜了。
5.93秒
這里有許多年種定義一個合約的方式:匿名(lambdas)、類 (classes)、簡單舊數據(plain ol' values)等。 我有個很長的case語句,用來檢測合約的類型。在此合約類型基礎之上,我可以做不同的事情。通過把它改為if語句,我節約了一些時間,但每次調用這個函數時,我仍然耗費了不必要的時間在仔細檢查這個判定樹上面:
if contract.is_a?(Class) # check argelsif 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來進行驗證,這樣就節約了1.2秒時間。
預計算一些其它的If語句,差不多又節省了1秒時間:
5.09秒
將.zip轉換為.times又為我節省了1秒時間:
結果證明:
args.zip(contracts).each do |arg, contract|
上面的代碼要比下面這個慢:
args.each_with_index do |arg, i|
要比下面這個更慢:
args.size.times do |i|
.zip要花費不必要的時間復制和創建新的數組。而我認為,.each_with_index之所以慢,是因為它受制于背后的.each,所以它涉及到兩個限制而不是一個。
4.23秒
下面再看些細節的東西,contracts庫在工作時,它會為每一個方法添加class_eval(class_eval要比define_method快)的新方法,這個新方法里有一個對老方法的引用,當調用新方法時,它會檢查參數,然后根據參數調用老方法,然后再檢查返回值,并且返回值。所有這些都會調用Contract class的check_args和check_result兩個方法。我取消了這兩個方法的調用,并且對新方法進行正確檢查,結果又節省了0.9秒:
2.94秒
在上面,我已經解釋了如何基于Contract類型創建lambda,然后使用這些來檢驗參數?,F在,我換了種方法,用生成代碼來替代,當我使用class_eval創建新方法時,它就會從eval中獲得結果。一個可怕的漏洞,但它避免了一大堆方法調用,并且節省了1.25秒:
1.57秒
最后,我改變了調用重寫方法的方式,我先前是使用引用:
# simplificationold_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}", nameclass_eval %{%{ def #{name}(*args)def #{name}(*args) self.send(:"original_#{name}", *args)self.send(:"original_#{name}", *args) endend}}
驚喜,又節省了1.4秒。我不知道為什么aliaa_method會如此地快,我猜是因為它跳過了一個方法的調用和綁定到.bindbind。
結果
我們成功的將時間從20秒優化到1.5秒,我不認為還有比這更好的結果的了。我所編寫的 這個測試腳本表明,一個被封裝過的add方法要比常規的add方法慢3倍,所以這些數字已經足夠好了。
想要驗證上面的結論很簡單,大量的時間花在調用方法上是只慢3倍的原因,這里有個更現實的例子:一個函數讀一個文件100000次:
稍微慢了點!add函數是個例外,我決定不再使用alias_method方法,因為它污染了命名空間,并且這些別名函數會到處出現(文檔、IDE的自動完成等)。
其它原因:
??? 在Ruby中調用方法很慢,我喜歡將代碼模塊化和重復使用,但或許是時候將更多的代碼進行內聯了。
??? 測試你的代碼!刪掉一個簡單的未使用的方法時間從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 endendendend
這樣可能會更加容易編寫decorator,并且運行速度也會加快。
2.關鍵字old
Ruby 2.0里缺乏的另一特性是引用重寫方法:
class Foo Foo def bardef bar 'Hello''Hello' endendend end class Fooclass Foo def bardef bar old + ' World'+ ' World' endendendend
Foo.new.bar # => 'Hello World'Foo.new.bar # => 'Hello World'
3.使用redef重新定義方法:
Matz曾說過:
??? 為了消除alias_method_chain,我們引入了Module#prepend,prepend前面加#號,這樣就沒機會在語言里加入冗余特性。
所以如果redef是冗余特征,也許prepend可以用來寫decorator?
4.其它實現
目前為止,這些都已經在YARV做過測試。
以上這些是武林技術頻道小編給大家介紹的優化Ruby代碼使程序運行速度提高的例子,閱讀了這篇文章,大家是否有所了解了呢?
新聞熱點
疑難解答
圖片精選