我是如何让 Ruby 项目提升 10 倍速度的 已翻译 100%

limichange 投递于 2013/08/20 06:52 (共 10 段, 翻译完成于 09-03)
阅读 5460
收藏 76
2
加载中

这篇文章是关于我怎样将我的ruby 珍宝contracts.ruby 提速10倍的。

contracts.ruby是我的一个项目,它用来为Ruby增加一些代码合约。它看起来像这样:

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

现在,只要add被调用,其参数与返回值都将会被检查。酷!

super0555
super0555
翻译于 2013/08/20 13:44
1

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倍)降速。我开始探究其中的原因。

super0555
super0555
翻译于 2013/08/20 13:50
1

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秒的结果。这个库已经超过原来两倍速了!

现在问题开始有点更为复杂了。


super0555
super0555
翻译于 2013/08/20 13:59
1

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)
super0555
super0555
翻译于 2013/08/21 13:30
1

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,所以它涉及到两个限制而不是一个。

super0555
super0555
翻译于 2013/08/20 14:10
1

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)
Garfielt
Garfielt
翻译于 2013/09/03 10:24
1

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)
super0555
super0555
翻译于 2013/08/21 13:41
1

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)
Legend___
Legend___
翻译于 2013/08/20 18:10
1

结果

我们设计是从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秒。
Garfielt
Garfielt
翻译于 2013/09/03 10:46
1

其他尝试的东西

方法选择器

Ruby2.0没有引入的一个特性是method combinators,这允许你这样写

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会让我做更加优化?

参考

Garfielt
Garfielt
翻译于 2013/09/03 10:58
1
本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接。
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。
加载中

评论(17)

开源小绵羊
开源小绵羊

引用来自“日谁改我名字了___”的评论

快10倍也是渣渣!! 要速度就用C !!

用合适的工具去做合适的事情。
哈哈__哈哈
哈哈__哈哈

引用来自“中山野鬼”的评论

引用来自“邓攀”的评论

引用来自“日谁改我名字了___”的评论

快10倍也是渣渣!! 要速度就用C !!

怎么渣渣了,你对ruby很熟悉?不要印象流,起码我用过的用ruby操作mysql数据库跟用ruby做爬虫爬取糗事百科的内容还有用ruby处理文本,速度一点点不慢,很快的,起码不比php java 使用这些功能慢,甚至快很多,不要印象流..
你要说纯粹比循环速度计算速度那就算了,我对这些纯粹的算法跟循环不熟悉而且使用不是很多..

哈,可不能这么说,可能你没见过快的。业务嘛,需求满足就好。没必要追求运行快,高级语言,开发快,是关键。不过要说速度。高级语言就别相互吵吵了。哈。复杂的代码,不谈编译,就仅仅是链接的时候,折腾折腾,也可能提升一倍的速度,这还是平均不是极端情况。

长知识了
Catelyn
Catelyn
按楼上的说法,被语言局限的人才是渣渣!
中山野鬼
中山野鬼

引用来自“邓攀”的评论

引用来自“日谁改我名字了___”的评论

快10倍也是渣渣!! 要速度就用C !!

怎么渣渣了,你对ruby很熟悉?不要印象流,起码我用过的用ruby操作mysql数据库跟用ruby做爬虫爬取糗事百科的内容还有用ruby处理文本,速度一点点不慢,很快的,起码不比php java 使用这些功能慢,甚至快很多,不要印象流..
你要说纯粹比循环速度计算速度那就算了,我对这些纯粹的算法跟循环不熟悉而且使用不是很多..

哈,可不能这么说,可能你没见过快的。业务嘛,需求满足就好。没必要追求运行快,高级语言,开发快,是关键。不过要说速度。高级语言就别相互吵吵了。哈。复杂的代码,不谈编译,就仅仅是链接的时候,折腾折腾,也可能提升一倍的速度,这还是平均不是极端情况。
WilsonHuang
WilsonHuang
看完感觉是标题党
哈哈__哈哈
哈哈__哈哈

引用来自“日谁改我名字了___”的评论

快10倍也是渣渣!! 要速度就用C !!

怎么渣渣了,你对ruby很熟悉?不要印象流,起码我用过的用ruby操作mysql数据库跟用ruby做爬虫爬取糗事百科的内容还有用ruby处理文本,速度一点点不慢,很快的,起码不比php java 使用这些功能慢,甚至快很多,不要印象流..
你要说纯粹比循环速度计算速度那就算了,我对这些纯粹的算法跟循环不熟悉而且使用不是很多..
橙汁儿
橙汁儿
没看懂,表示
TX
TX
这时间统计是用啥工具得到的?
rubyist
rubyist
肯定用的1.8,用1.9以上不会这样
pobeike
pobeike
Ruby2.0没有引入的一个特性是方法选择器,这运行你这样写
也许Rubinius会让我做更加优化?

是否该说神翻译?
返回顶部
顶部