首頁»RubyOnRails»我是如何讓 Ruby 項目提升 10 倍速度的

                    我是如何讓 Ruby 項目提升 10 倍速度的

                    來源:oschina 發布時間:2013-09-03 閱讀次數:

                      這篇文章主要介紹了我是如何把ruby gem contracts.ruby速度提升10倍的。

                      contracts.ruby是我的一個項目,它用來為Ruby增加一些代碼合約。它看起來像這樣:

                    Contract Num, Num => Num
                    def add(a, b)
                      a + b
                    end

                      現在,只要add被調用,其參數與返回值都將會被檢查。酷!

                     20 秒

                      本周末我校驗了這個庫,發現它的性能非常糟糕。

                                                         user     system      total        real
                    testing add                      0.510000   0.000000   0.510000 (  0.509791)
                    testing contracts add           20.630000   0.040000  20.670000 ( 20.726758)

                      這是在隨機輸入時,運行兩個函數1,000,000次以后的結果。

                      所以給一個函數增加合約最終將引起極大的(40倍)降速。我開始探究其中的原因。

                     8 秒

                      我立刻就獲得了一個極大的進展。當一個合約傳遞的時候,我調用了一個名為success_callback的函數。這個函數是完全空的。這是它的完整定義:

                    def self.success_callback(data)
                    end  

                      這是我歸結為“僅僅是案例”(未來再驗證!)的一類。原來,函數調用在Ruby中代價十分昂貴。僅僅刪除它就節約了8秒鐘!

                                                         user     system      total        real
                    testing add                      0.520000   0.000000   0.520000 (  0.517302)
                    testing contracts add           12.120000   0.010000  12.130000 ( 12.140564)

                      刪除許多其他附加的函數調用,我有了9.84-> 9.59-> 8.01秒的結果。這個庫已經超過原來兩倍速了!

                      現在問題開始有點更為復雜了。

                     5.93 秒

                      有多種方法來定義一個合約:匿名(lambdas),類 (classes), 簡單舊數據(plain ol’ values), 等等。我有個很長的case語句,用來檢測它是什么類型的合約。在此合約類型基礎之上,我可以做不同的事情。通過把它改為if語句,我節約了一些時間,但每次這個函數調用時,我仍然耗費了不必要的時間在穿越這個判定樹上面:

                    if contract.is_a?(Class)
                      # check arg
                    elsif contract.is_a?(Hash)
                      # check arg
                    ...

                      我將其修改為合約定義的時候,以及創建lambdas的時候,只需一次穿越樹:

                    if contract.is_a?(Class)
                      lambda { |arg| # check arg }
                    elsif contract.is_a?(Hash)
                      lambda { |arg| # check arg }
                    ...

                      之后我通過將參數傳遞給這個預計算的lambda來進行校驗,完全繞過了邏輯分支。這又節約了1.2秒。

                                                         user     system      total        real
                    testing add                      0.510000   0.000000   0.510000 (  0.516848)
                    testing contracts add            6.780000   0.000000   6.780000 (  6.785446)

                      預計算一些其它的if語句幾乎又節約1秒鐘:

                                                         user     system      total        real
                    testing add                      0.510000   0.000000   0.510000 (  0.516527)
                    testing contracts add            5.930000   0.000000   5.930000 (  5.933225)

                     5.09 秒

                      斷開.zip的.times為我幾乎又節約了一秒鐘:

                                                         user     system      total        real
                    testing add                      0.510000   0.000000   0.510000 (  0.507554)
                    testing contracts add            5.090000   0.010000   5.100000 (  5.099530)

                      原來,

                    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快)。這個新方法中包含了一個到舊方法的引用。當新方法被調用時,它檢查參數,然后使用這些參數調用老方法,然后檢查返回值,最后返回返回值。所有這些調用contractclass:check_args和check_result兩個方法。我去除了這兩個方法的調用,在新方法中檢查是否正確。這樣我又節省了0.9秒:

                                                         user     system      total        real
                    testing add                      0.530000   0.000000   0.530000 (  0.523503)
                    testing contracts add            4.230000   0.000000   4.230000 (  4.244071)

                     2.94 秒

                      之前我曾經解釋過,我是怎樣在合約類型基礎之上創建lambdas,之后再用它們來檢測參數。我換了一種方法,用生成代碼來替代,當我用class_eval來創建新的方法時,它就會從eval獲得結果。一個糟糕的漏洞!但它避免了一大堆方法調用,并且為我又節省了1.25秒。

                                                         user     system      total        real
                    testing add                      0.520000   0.000000   0.520000 (  0.519425)
                    testing contracts add            2.940000   0.000000   2.940000 (  2.942372)

                     1.57秒

                      最后,我改變了調用重寫方法的方式。我之前的方法是使用一個引用:

                    # simplification
                    old_method = method(name)
                    
                    class_eval %{
                        def #{name}(*args)
                            old_method.bind(self).call(*args)
                        end
                    }

                      我把方法調用改成了 alias_method的方式:

                    alias_method :"original_#{name}", name
                    class_eval %{
                        def #{name}(*args)
                            self.send(:"original_#{name}", *args)
                          end
                    }

                      這帶給了我1.4秒的驚喜。我不知道為什么 alias_method is這么快...我猜測可能是因為跳過了方法調用和綁定

                                                         user     system      total        real
                    testing add                      0.520000   0.000000   0.520000 (  0.518431)
                    testing contracts add            1.570000   0.000000   1.570000 (  1.568863)

                     結果

                      我們設計是從20秒到1.5秒!是否可能做得比這更好呢?我不這么認為。我寫的這個測試腳本表明,一個包裹的添加方法將比定期添加方法慢3倍,所以這些數字已經很好了。

                      方法很簡單,更多的時間花在調用方法是只慢3倍的原因。這是一個更現實的例子:一個函數讀文件100000次:

                                                         user     system      total        real
                    testing read                     1.200000   1.330000   2.530000 (  2.521314)
                    testing contracts read           1.530000   1.370000   2.900000 (  2.903721)

                     慢了很小一點!我認為大多數函數只能看到稍慢一點,addfunction是個例外。

                     我決定不使用alias_method,因為它污染命名空間而且那些別名函數會到處出現(文檔,IDE的自動完成等)。

                     一些額外的:

                    1. Ruby中方法調用很慢,我喜歡將我的代碼模塊化的和重復使用,但也許是我開始內聯代碼的時候了。
                    2. 測試你的代碼!刪掉一個簡單的未使用的方法花費我20秒到12秒。

                     其他嘗試的東西

                      方法選擇器

                      Ruby2.0沒有引入的一個特性是方法選擇器,這運行你這樣寫

                    class Foo
                      def bar:before
                        # will always run before bar, when bar is called
                      end
                    
                      def bar:after
                        # will always run after bar, when bar is called
                        # may or may not be able to access and/or change bar's return value
                      end
                    end

                      這使寫裝飾器更容易,而且可能更快。

                      keywordold

                      Ruby2.0沒有引入的另一個特性,這允許你引用一個重寫方法:

                    class Foo
                      def bar
                        'Hello'
                      end
                    end 
                    
                    class Foo
                      def bar
                        old + ' World'
                      end
                    end
                    
                    Foo.new.bar # => 'Hello World'

                      使用redef重新定義方法

                      這個Matz說過:

                    To eliminatealias_method_chain, we introducedModule#prepend. There’s no chance to add redundant feature in the language.

                      所以如果redef是冗余的特征,也許prepend可以用來寫修飾器了?

                      其他的實現

                      到目前為止,所有這一切都已經在YARV上測試過。也許Rubinius會讓我做更加優化?

                     參考

                      原文地址:http://www.adit.io/posts/2013-03-04-How-I-Made-My-Ruby-Project-10x-Faster.html

                    QQ群:WEB開發者官方群(515171538),驗證消息:10000
                    微信群:加小編微信 849023636 邀請您加入,驗證消息:10000
                    提示:更多精彩內容關注微信公眾號:全棧開發者中心(fsder-com)
                    網友評論(共1條評論) 正在載入評論......
                    理智評論文明上網,拒絕惡意謾罵 發表評論 / 共1條評論
                    登錄會員中心
                    福彩试机号今天 欢乐斗地主卡主 北京十一选五昨天开奖结果 极速11选5投注技巧 香港六合彩网站 广东十一选五杀号网易 澳洲三分彩计划软件 彩票中大奖的心情 福建快三遗漏值统计 江西多乐彩形态走势图 免费麻将牌游戏 香港六合彩的预测 双色球走势图浙江风采 江苏e球彩中奖金额多少钱 泳坛夺金选号技巧 老快3遗漏号数据