优化Ruby代码使程序运行速度提高的例子

这篇文章主要介绍了我是如何把ruby gem contracts.ruby速度提升10倍的。

contracts.ruby在我项目里用来添加代码合约(code contracts)到Ruby中。看起来差不多是这样的:

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

只要add方法被调用,参数和返回值都会被检查。

20秒

本周末,我对该库进行了测试,发现其性能非常糟:

这是在随机输入下,运行1000次以后的结果。

所以,当给一个函数加入合约功能后,运行速度明显下降(约40倍这样),对此,我进行了深入的研究。

8秒

我取得了较大的进展,当传递合约时,我调用success_callback函数,该函数是个空函数,下面是这个函数的整个定义:

def self.success_callback(data)
end

原来函数调用在Ruby中是非常昂贵的,仅删除这个调用,就节省了8秒钟:

删除其它一些附件函数的调用,时间花费开始从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
...

当定义合约和构建lambda时,对树只做一次检查:

if contract.is_a?(Class)
 lambda { |arg| # check arg }
elsif contract.is_a?(Hash)
 lambda { |arg| # check arg }

然后,我将完全绕过逻辑分支,通过将参数传递给预计算的lambda来进行验证,这样就节约了1.2秒时间。

预计算一些其它的If语句,差不多又节省了1秒时间:

5.09秒

将.zip转换为.times又为我节省了1秒时间:

结果证明:

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快)的新方法,这个新方法里有一个对老方法的引用,当调用新方法时,它会检查参数,然后根据参数调用老方法,然后再检查返回值,并且返回值。所有这些都会调用Contract class的check_args和check_result两个方法。我取消了这两个方法的调用,并且对新方法进行正确检查,结果又节省了0.9秒:

2.94秒

在上面,我已经解释了如何基于Contract类型创建lambda,然后使用这些来检验参数。现在,我换了种方法,用生成代码来替代,当我使用class_eval创建新方法时,它就会从eval中获得结果。一个可怕的漏洞,但它避免了一大堆方法调用,并且节省了1.25秒:

1.57秒

最后,我改变了调用重写方法的方式,我先前是使用引用:

# simplification
old_method = method(name)= method(name)

class_eval %{%{
  def #{name}(*args)def #{name}(*args)
    old_method.bind(self).call(*args).bind(self).call(*args)
  endend
}}

我进行了修改,并使用alias_method方法:

alias_method :"original_#{name}", name:"original_#{name}", name
class_eval %{%{
  def #{name}(*args)def #{name}(*args)
    self.send(:"original_#{name}", *args)self.send(:"original_#{name}", *args)
   endend
}}

惊喜,又节省了1.4秒。我不知道为什么aliaa_method会如此地快,我猜是因为它跳过了一个方法的调用和绑定到.bindbind。

结果

我们成功的将时间从20秒优化到1.5秒,我不认为还有比这更好的结果的了。我所编写的 这个测试脚本表明,一个被封装过的add方法要比常规的add方法慢3倍,所以这些数字已经足够好了。

想要验证上面的结论很简单,大量的时间花在调用方法上是只慢3倍的原因,这里有个更现实的例子:一个函数读一个文件100000次:

稍微慢了点!add函数是个例外,我决定不再使用alias_method方法,因为它污染了命名空间,并且这些别名函数会到处出现(文档、IDE的自动完成等)。

其它原因:

在Ruby中调用方法很慢,我喜欢将代码模块化和重复使用,但或许是时候将更多的代码进行内联了。
    测试你的代码!删掉一个简单的未使用的方法时间从20秒缩短到了12秒。

其它尝试

1.方法选择器

Ruby 2.0里缺少方法选择器这一特性,否则你还可以这样写:

class Foo Foo
 def bar:beforedef bar:before
  # will always run before bar, when bar is called# will always run before bar, when bar is called
 endend

 def bar:afterdef bar:after
  # will always run after bar, when bar is called# will always run after bar, when bar is called
  # may or may not be able to access and/or change bar's return value# may or may not be able to access and/or change bar's return value
 endend
endend

这样可能会更加容易编写decorator,并且运行速度也会加快。

2.关键字old

Ruby 2.0里缺乏的另一特性是引用重写方法:

class Foo Foo
 def bardef bar
  'Hello''Hello'
 endend
end end 

class Fooclass Foo
 def bardef bar
  old + ' World'+ ' World'
 endend
endend

Foo.new.bar # => 'Hello World'Foo.new.bar # => 'Hello World'

3.使用redef重新定义方法:

Matz曾说过:

为了消除alias_method_chain,我们引入了Module#prepend,prepend前面加#号,这样就没机会在语言里加入冗余特性。

所以如果redef是冗余特征,也许prepend可以用来写decorator?

4.其它实现

目前为止,这些都已经在YARV做过测试。

(0)

相关推荐

  • 在Ruby中处理文件的输入和输出的教程

    Ruby 提供了一整套 I/O 相关的方法,在内核(Kernel)模块中实现.所有的 I/O 方法派生自 IO 类. 类 IO 提供了所有基础的方法,比如 read. write. gets. puts. readline. getc 和 printf. 本章节将讲解所有 Ruby 中可用的基础的 I/O 函数.如需了解更多的函数,请查看 Ruby 的 IO 类. puts 语句 在前面的章节中,您赋值给变量,然后使用 puts 语句打印输出. puts 语句指示程序显示存储在变量中的值.这将在

  • Ruby中操作文件的方法介绍

    Ruby提供了一套完整的I/O相关的内核模块中实现方法.所有I/O方法来自IO类. 类IO提供了所有的基本方法,如 read, write, gets, puts, readline, getc 和 printf. 本章将涵盖所有可供在Ruby中使用的基本I/O功能.如需使用更多的功能,请参考Ruby的IO类. puts 语句: 在前面的章节中,你指定值的变量和然后使用声明 puts 输出. puts 把语句指示程序显示存储在变量值.这将添加一个新行,每行末尾写出(输出). 例子: #!/usr

  • 详解Ruby中的异常

    异常和执行总是被联系在一起.如果您打开一个不存在的文件,且没有恰当地处理这种情况,那么您的程序则被认为是低质量的. 如果异常发生,则程序停止.异常用于处理各种类型的错误,这些错误可能在程序执行期间发生,所以要采取适当的行动,而不至于让程序完全停止. Ruby 提供了一个完美的处理异常的机制.我们可以在 begin/end 块中附上可能抛出异常的代码,并使用 rescue 子句告诉 Ruby 完美要处理的异常类型. 语法 begin # - rescue OneTypeOfException #

  • 优化Ruby代码使程序运行速度提高的例子

    这篇文章主要介绍了我是如何把ruby gem contracts.ruby速度提升10倍的. contracts.ruby在我项目里用来添加代码合约(code contracts)到Ruby中.看起来差不多是这样的: Contract Num, Num => Num def add(a, b) a + b end 只要add方法被调用,参数和返回值都会被检查. 20秒 本周末,我对该库进行了测试,发现其性能非常糟: 这是在随机输入下,运行1000次以后的结果. 所以,当给一个函数加入合约功能后,

  • 优化Python代码使其加快作用域内的查找

    我将示范微优化(micro optimization)如何提升python代码5%的执行速度.5%!同时也会触怒任何维护你代码的人. 但实际上,这篇文章只是解释一下你偶尔会在标准库或者其他人的代码中碰到的代码.我们先看一个标准库的例子,collections.OrderedDict类: def __setitem__(self, key, value, dict_setitem=dict.__setitem__): if key not in self: root = self.__root l

  • 一行代码让 Python 的运行速度提高100倍

    python一直被病垢运行速度太慢,但是实际上python的执行效率并不慢,慢的是python用的解释器Cpython运行效率太差. "一行代码让python的运行速度提高100倍"这绝不是哗众取宠的论调. 我们来看一下这个最简单的例子,从1一直累加到1亿. 最原始的代码: import time def foo(x,y): tt = time.time() s = 0 for i in range(x,y): s += i print('Time used: {} sec'.form

  • 优化PHP代码技巧的小结

    优化PHP代码技巧的小结1. 如果一个方法能被静态,那就声明他为静态的,速度可提高 1/4;2. echo 的效率高于 print,因为 echo 没有返回值,print 返回一个整型;3. 在循环之前设置循环的最大次数,而非在在循环中;4. 销毁变量去释放内存,特别是大的数组;5. 避免使用像__get, __set, __autoload 等魔术方法;6. requiere_once()比较耗资源;7. 在 includes 和 requires 中使用绝对路径,这样在分析路径花的时间更少;

  • 对优化Ruby on Rails性能的一些办法的探究

    1.导致你的 Rails 应用变慢无非以下两个原因: 在不应该将 Ruby and Rails 作为首选的地方使用 Ruby and Rails.(用 Ruby and Rails 做了不擅长做的工作) 过度的消耗内存导致需要利用大量的时间进行垃圾回收. Rails 是个令人愉快的框架,而且 Ruby 也是一个简洁而优雅的语言.但是如果它被滥用,那会相当的影响性能.有很多工作并不适合用 Ruby and Rails,你最好使用其它的工具,比如,数据库在大数据处理上优势明显,R 语言特别适合做统计

  • 浅析使用Turck-mmcache编译来加速、优化PHP代码

    php_screw确实很不错,但是只能起到加密的作用,并没有加速和优化的作用.下面我们来看看:Turck MMCache它的作用是通过对php代码的编译来加速.优化php代码,我们如果发布的php代码是经过编译的,也多少能起到一些对代码的保护作用.下面我们对compute.php进行编译看看前后的代码区别,下面是compute.php的源代码<? //============用来循环计算的compute.php文件================// $a=0; $t=time(); for($

  • SQL Server 聚焦存储过程性能优化、数据压缩和页压缩提高IO性能方法(一)

    前言 关于SQL Server基础系列尚未结束,还剩下最后一点内容未写,后面会继续.有园友询问我什么时候开始写SQL Server性能系列,估计还得等一段时间,最近工作也比较忙,但是会陆陆续续的更新SQL Server性能系列,本篇作为性能系列的基本引导,让大家尝尝鲜.在涉及到SQL Server性能优化时,我看到的有些文章就是一上来列出SQL Server的性能优化条例,根本没有弄清楚为什么这么做,当然也有可能是自己弄懂了,只是作为备忘录,但是到了我这里,我会遵循不仅仅是备忘录,还要让各位园友

  • 使用reactjs优化了进度条页面性能提高70%

    目录 前言 进度条的应用场景 不推荐的写法 推荐的写法 缺陷 缺陷:这两种方案都会引发频繁的重排和重绘 重排: 重绘: 极致的优化 小彩蛋 「优化前」 「优化后」 结尾 前言 大家好,我是零一.最近我准备在组里进行代码串讲,所以我梳理了下项目之前的业务代码.在梳理的过程中,我看到了有个进度条组件写的非常好,这又想起我刚开始学前端时写的进度条的代码,跟这个比起来真的差距太大了(大部分的初学者应该都想不到,而且我第一次家实习公司带我的mentor亦是如此). 因此,我想给大家分享一下这个思路极好的进

  • c#通过app.manifest使程序以管理员身份运行

    微软在Windows Vista开始引入了UAC(用户帐户控制)新技术(点击这儿了解什么是UAC).当程序执行时需要权限的话,UAC会弹出警告消息:让用户确认是否同意允许这个程序改变你的计算机配置,windows需要得到你的许可才可以让程序继续执行下去. 通常我们使用c#编写的程序不会弹出这个提示,也就无法以管理员身分运行.微软的操作系统使用微软的产品方法当然是有的,通过app.manifest配置可以使程序打开的时候,弹出UAC提示需要得到允许才可以继续,这样就获得了管理员的权限来执行程序.

  • 易语言制作绝地求生视野一键优化的代码

    绝地求生一键优化的代码 优化后远处树干不显示,地面光滑,更容易看到远处的敌人. .版本 2 .支持库 shell .支持库 spec .支持库 iext .程序集 窗口程序集_启动窗口 .子程序 __启动窗口_创建完毕 路径编辑框.内容 = 取特定目录 (8) + "..\Local\TslGame\Saved\Config\WindowsNoEditor\" .如果 (文件是否存在 (路径编辑框.内容 + "\一键优化备份\")) 还原按钮.禁止 = 假 .否则

随机推荐