这篇文章主要介绍了我是如何把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"htmlcode">if contract.is_a"2015410153721524.jpg (654×126)" src="/UploadFiles/2021-04-08/2015410153721524.jpg">预计算一些其它的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 endendFoo.new.bar # => 'Hello World'Foo.new.bar # => 'Hello World'
3.使用redef重新定义方法:
Matz曾说过:
为了消除alias_method_chain,我们引入了Module#prepend,prepend前面加#号,这样就没机会在语言里加入冗余特性。
所以如果redef是冗余特征,也许prepend可以用来写decorator?
4.其它实现
目前为止,这些都已经在YARV做过测试。
Ruby
免责声明:本站文章均来自网站采集或用户投稿,网站不提供任何软件下载或自行开发的软件! 如有用户或公司发现本站内容信息存在侵权行为,请邮件告知! 858582#qq.com
RTX 5090要首发 性能要翻倍!三星展示GDDR7显存
三星在GTC上展示了专为下一代游戏GPU设计的GDDR7内存。
首次推出的GDDR7内存模块密度为16GB,每个模块容量为2GB。其速度预设为32 Gbps(PAM3),但也可以降至28 Gbps,以提高产量和初始阶段的整体性能和成本效益。
据三星表示,GDDR7内存的能效将提高20%,同时工作电压仅为1.1V,低于标准的1.2V。通过采用更新的封装材料和优化的电路设计,使得在高速运行时的发热量降低,GDDR7的热阻比GDDR6降低了70%。
更新日志
- 雨林唱片《赏》新曲+精选集SACD版[ISO][2.3G]
- 罗大佑与OK男女合唱团.1995-再会吧!素兰【音乐工厂】【WAV+CUE】
- 草蜢.1993-宝贝对不起(国)【宝丽金】【WAV+CUE】
- 杨培安.2009-抒·情(EP)【擎天娱乐】【WAV+CUE】
- 周慧敏《EndlessDream》[WAV+CUE]
- 彭芳《纯色角3》2007[WAV+CUE]
- 江志丰2008-今生为你[豪记][WAV+CUE]
- 罗大佑1994《恋曲2000》音乐工厂[WAV+CUE][1G]
- 群星《一首歌一个故事》赵英俊某些作品重唱企划[FLAC分轨][1G]
- 群星《网易云英文歌曲播放量TOP100》[MP3][1G]
- 方大同.2024-梦想家TheDreamer【赋音乐】【FLAC分轨】
- 李慧珍.2007-爱死了【华谊兄弟】【WAV+CUE】
- 王大文.2019-国际太空站【环球】【FLAC分轨】
- 群星《2022超好听的十倍音质网络歌曲(163)》U盘音乐[WAV分轨][1.1G]
- 童丽《啼笑姻缘》头版限量编号24K金碟[低速原抓WAV+CUE][1.1G]