1. <em id="2qvri"><tr id="2qvri"></tr></em>
      1. 首頁»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條評論
        登錄會員中心
        江苏快3投注技巧