翻译 如何在源码阶段写出更快的 Ruby

etnl · 发布于 2015年5月25日 · 最后由 jjzxcc 回复于 2015年6月24日 · 4813 次阅读
11955
本帖已被设为精华帖!

此文章翻译自How To Write Ruby Faster at the Source Code Level, 本人水平有限,错误之处,还望各位指正。


优化可以采取许多不同的形式,但程序员关注何处以及如何开发项目的过程中进行优化。这个讲演来自 RubyConf India 2015, 总部位于柏林的 SoundCloud 开发者 Erik Michaels-Ober(twittergithub) 对于在源码这个层级如何提升 Ruby 性能优化,进行了一次演讲。

Michaels-Ober首先注意到的是,大多数开发人员对过早的性能优化有偏见。他引用了斯坦福教授、算法设计和分析之父 Donald Knuth,在 1974 年的一句话 "过早的优化乃万恶之源"。Michaels-Ober指出适当的但不合时宜的优化的确会让代码“丑陋,难以阅读和更复杂”,所以只有在必要或合时宜时,进行代码优化。

什么时候进行优化呢?这里,Michaels-Ober再次引用Knuth,他有两个标准:第一,优化可以轻易获得;第二,优化的好处巨大(Knuth给出12%的阈值)。Michaels-Ober增加了第三个标准,借用自 Ruby 的主设计者Matz(Yukihiro Matsumoto): Ruby 为程序员快乐而生,所以优化也应使程序员感到快乐 -- 不论是让代码更“漂亮”或拥有更好的性能。

优化层次

Michaels-Ober接着概述了可能适用于任何语言和项目的不同层次的优化:设计、源码、构建、编译和运行时。Michaels-Ober的重点是源码优化。再次搬出 Knuth ,大神认为:“程序员的直觉预测电脑会如何解释的代码、是否优化时会出现偏差”。 “对于代码的快慢,我们没有很好的直觉” Michaels-Ober说到。“我们常常犯错,所以如果过早的优化,或许你认为你在优化它,但实际上你在写一个非最优代码”。

那么如果知道哪个版本是更优的呢? “你必须跑个分”, Michaels-Ober 说到。Ruby 内置基准测试库允许对一个方法运行指定的次数。然而 Michaels-Ober 指出内置基准测试的问题是你得猜运行多少次才是明显的提升。

作为替代,Michaels-Ober建议使用 benchmark-ips(iteratorions per second), 它使用每秒钟运行次数而不是每次运行秒数。每个版本的代码运行五秒,会生成一个代码运行次数的“报告”。 “5秒时间是一个足够长的时间,足以移除统计误差;运行次数越多,误差会越低,所以不必担心这些误差”。

在代码阶段写出更快的 Ruby

Michaels-Ober 接下来展示了一些如何简化代码来达到运行更快、更易读的例子(你可以参照 Speaker Deck 主题幻灯)。

Block vs. Symbol#to_proc

Symbol#to_problock快20倍,因为 Ruby 解释器内部做了优化。Symbol#to_proc最初作为 RAILS 快捷方法,后来添加到了 Ruby。Ruby 知道如何运行Symbol#to_proc,并在内部进行优化。

Enumerable#map and Array#flatten vs. Enumerable#flat_map

map会返回一个列表的列表。如果想让一个列表map然后再flatten,可以使用可读性更好的flat_map代替mapflatten,俩工作方式相同,且flat_map快4.5倍,因为它只迭代一次,而不是两次。

Enumerable#reverse and Enumerable#each vs. Enumerable#reverse_each

reverse_each不会拷贝列表,只是反方向迭代。这使得它比另一种方法快 17%。

Hash#keys and Enumerable#each vs. Hash#each_key

Ruby 有个内置方法创建哈希键列表。更快的方法是不立即创建列表,而只是返回键的值,这让它快了33%。

Array#shuffle and Array#first vs. Array#sample

假设有个列表,想随机的提取一些值。一个方法是使用array.shuffle.first,打乱列表然后取列表的第一个元素,或者使用array.sample,它比前一个方法快 15 倍。

Hash#merge vs. Hash#merge!

不可变版本修改哈希对比可变版本创建一个哈希的拷贝,再进行合并和拷贝。因为在更小的空间内修改哈希,所以快了 3 倍。同样适用于Hash#[],快 2 倍速度。

Hash#fetch vs. Hash#fetch with block

fetch第二个参数是 block 要比直接传递 block 的结果快 2 倍。

String#gsub vs. String#sub

gsub将字符中所有的第一个字符替换成第二个字符。如果你只想进行一次替换,也不扫描多余的字符。使用sub快 50%。

String#gsub vs. String#tr

想进行全部替换时,总是可以使用tr替代gsub,使用tr快 5 倍。

Parallel vs. sequential assignment

并行赋值需要分配列表,且可读性不高。并行赋值在交换值的时候很有用,除此之外,使用顺序赋值,它可读性更好、快 40 %。

Throw/catch

exceptions相比,使用throw/cath控制流跳转,它快 5 倍。

Michaels-Ober说到:“尽管其他的优化可以在下一个代码编译阶段完成,但上面这些源码的优化技术可以快速、无痛的完成,可以使 Ruby 代码更快、性能更好、可读性更高”。


如果上面这几个不过瘾的话,大力推荐 fast-ruby 。感谢 @JuanitoFatas

共收到 20 条回复
9770

很赞!比如merge gsub 等等平时就用的比较多,惭愧啊!

1286

赞一个

4楼 已删除
16154

:plus1:

96

感谢分享!好

10316

Symbol#to_proc 比block快20倍

涨知识了。。。

8楼 已删除
3981

谢谢分享!!

8744

#1楼 @clarkyi merge! 是比较快的吧

9770

@lithium4010 手误,已更新! 😊

96

涨姿势了

96

感觉应该是「在代码(这个)层级」而不是「在代码阶段」吧…

6291

:plus1:

96

:plus1:

12381

涨姿势,很实用,不错,而且个人觉得ruby2.x版本的性能其实不差

96

感觉读起来像《Effective Ruby》

22楼 已删除
2492

:plus1:

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册