我是如何讓 Ruby 專案提升 10 倍速度的
這篇文章主要介紹了我是如何把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的自動完成等)。
一些額外的:
- Ruby中方法呼叫很慢,我喜歡將我的程式碼模組化的和重複使用,但也許是我開始內聯程式碼的時候了。
- 測試你的程式碼!刪掉一個簡單的未使用的方法花費我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會讓我做更加優化?
參考
- Ruby MRI的六個優化要點。
- 從這裡檢出contracts.ruby。
-
如果你已經用過contracts.ruby,升級到v0.2.1體驗速度的提升。
原文地址:http://www.adit.io/posts/2013-03-04-How-I-Made-My-Ruby-Project-10x-Faster.html
相關文章
- 從 PHP 到 Go:我們是如何將 API 速度提升 8 倍PHPGoAPI
- 如何讓webpack打包的速度提升50%?Web
- 愛奇藝編碼團隊:我們讓AV1編碼速度提升5倍
- 如何將 iOS 專案的編譯速度提高 5 倍iOS編譯
- 嫌 OSS 查詢太慢?看我們如何將速度提升 10 倍!
- 如何將 MySQL 查詢速度提升 300 倍MySql
- 機器學習演算法讓TCP傳輸速度提升一倍機器學習演算法TCP
- 如何將 iOS 工程打包速度提升十倍以上iOS
- 【提升學習力】如何讓學習效果提升 N 倍
- [貝聊科技]如何將 iOS 專案的編譯速度提高5倍iOS編譯
- 用好 ChatGPT,讓你工作效率提升10倍ChatGPT
- 使用webpack構建時,如何使你的專案打包速度提升68% ?Web
- 我是如何在公司專案中使用ESLint來提升程式碼質量的EsLint
- 使用 Webpack 的 DllPlugin 提升專案構建速度WebPlugin
- 讓你的 webpack sass 和 css 處理效能 10 倍提升WebCSS
- 我是如何降低專案的溝通成本?
- Flurry:智慧手機普及速度是PC的10倍 網際網路的2倍
- 我們的創業專案是如何夭折的創業
- 如何將Python自然語言處理速度提升100倍:用spaCy/Cython加速NLPPython自然語言處理
- 我是如何使計算提速>150倍的
- 如何提升網站速度網站
- 使用 happypack 提升 Webpack 專案構建速度APPWeb
- 使用子查詢可提升 COUNT DISTINCT 速度 50 倍
- ICLR 2024 | 無需訓練,Fast-DetectGPT讓文字檢測速度提升340倍ICLRASTGPT
- 我們是如何將一個專案做爛的
- 《前端演算法系列》如何讓前端程式碼速度提高60倍前端演算法
- 存算分離下寫效能提升10倍以上,EMR Spark引擎是如何做到的?Spark
- 如何把 MySQL 備份驗證效能提升 10 倍MySql
- 神經渲染與AI生成框架結合,5倍提升遊戲速度,英偉達是這樣做的AI框架遊戲
- DeepMind丟掉了歸一化,讓影像識別訓練速度提升了8.7倍 | 已開源
- 如何提升前端開發速度前端
- 你的Mac有了專用版TensorFlow,GPU可用於訓練,速度最高提升7倍MacGPU
- 小程式redux效能優化,提升三倍渲染速度Redux優化
- 史上首個實時AI影片生成技術:DiT通用,速度提升10.6倍AI
- 我是如何將一個老系統的kafka消費者服務的效能提升近百倍的Kafka
- Ruby 將引入新 JIT 編譯器:YJIT,平均速度提升 23%編譯
- 如何提升專案的運營和管理?
- 這個應用魔方厲害了,讓軟體開發者效率提升10倍