龙都娱乐»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的自动完成等)。  一些额外的:
  • 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会让我做更加优化?

     参考

    1. Ruby MRI的六个优化要点
    2. 当monkey打包了一个方法,你能在新实现里调用这个复写方法么?
    3. 从这里检出contracts.ruby
    4. 如果你已经用过contracts.ruby,升级到v0.2.1体验速度的提升。
      原文地址:http://www.adit.io/posts/2013-03-04-How-I-Made-My-Ruby-Project-10x-Faster.html
    QQ群: WEB龙都娱乐官方总群(83010142) 加群密码:关注下方微信公众号,发送消息 mm 获取
    提示:更多精彩内容关注微信公众号:全栈龙都娱乐中心(fsder-com)
    网友评论(共1条评论) 正在载入评论......
    理智评论文明上网,拒绝恶意谩骂 发表评论 / 共1条评论
    登录会员中心
    龙都娱乐