首页»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开发者官方总群(83010142) 加群密码:关注下方微信公众号,发送消息 mm 获取
提示:更多精彩内容关注微信公众号:全栈开发者中心(admin10000_com)
网友评论(共1条评论) 正在载入评论......
理智评论文明上网,拒绝恶意谩骂 发表评论 / 共1条评论
登录会员中心