Ruby Performance Differences in Ruby

larrylv · May 12, 2014 · Last by huanzhang replied at June 24, 2015 · 9144 hits
Topic has been selected as the excellent topic by the admin.

http://blog.larrylv.com/performance-differences-in-ruby/

http://parley.rubyrogues.com/t/dramatic-performance-differences-in-ruby/2151

前几天 @sferik 在 Parley 上说最近在准备一个关于 Performance in Ruby 的 talk,发贴讨论了 Ruby 中哪些写法会造成性能的巨大提升,我顺手整理了下帖子内容并实验了一下。

Benchmark 环境:MacBook Air Mid 2012, Ruby 2.1.1, gem benchmark-ips.

Proc#call versus yield

require 'benchmark/ips'

def slow(&block)
  block.call
end

def fast
  yield
end

Benchmark.ips do |x|
  x.report("slow") { slow { 1 + 1 } }
  x.report("fast") { fast { 1 + 1 } }
end
slow   770263.8 (±4.8%) i/s -    3849832 in   5.010201s (5 秒钟可运行 3849832 次)
fast  3985294.7 (±8.7%) i/s -   19751563 in   5.001024s (5 秒钟可运行 19751563 次)

从上面的 benchmark 结果中可以看出两种写法的显著性能差异,这主要是因为第一种写法中要不断的创建 Proc 对象赋给 block 参数导致的。

Enumerable#map and Array#flatten versus Enumerable#flat_map

def slow
  (1..50).map{ |i| i.divmod(7) }.flatten
end

def fast
  (1..50).flat_map{ |i| i.divmod(7) }
end
slow   32514.2 (±6.8%) i/s -     162134 in   5.014068s
fast    57858.6 (±7.3%) i/s -     292463 in   5.084633s

Hash#merge versus Hash#merge! (bang methods, in general)

def slow
  (1..10).inject({}) { |h, e| h.merge(e => e) }
end

def fast
  (1..10).inject({}) { |h, e| h.merge!(e => e) }
end
slow    22054.4 (±6.3%) i/s -     110081 in   5.012558s
fast     75393.0 (±9.3%) i/s -     375577 in   5.028409s

Hash#merge! versus Hash#[]=

def slow
  (1..10).inject({}) { |h, e| h.merge!(e => e) }
end

def fast
  (1..10).inject({}) { |h, e| h[e] = e; h }
end
slow    72613.7 (±9.9%) i/s -     364662 in   5.082934s
fast   158245.6 (±7.1%) i/s -     796005 in   5.056857s

Hash#fetch with second argument versus Hash#fetch with block

def slow
  {:foo => :bar}.fetch(:foo, (1..10).to_a)
end

def fast
  {:foo => :bar}.fetch(:foo) { (1..10).to_a }
end
slow   412806.8 (±11.2%) i/s -    2037520 in   5.009145s
fast  1134439.1  (±8.1%) i/s -    5662160 in   5.027080s

String#gsub versus String#sub

def slow
  'http://parley.rubyrogues.com/'.gsub(%r{\Ahttp://}, 'https://')
end

def fast
  'http://parley.rubyrogues.com/'.sub(%r{\Ahttp://}, 'https://')
end
slow   237660.4 (±5.0%) i/s -    1190664 in   5.023559s
fast   320335.6 (±5.1%) i/s -    1614839 in   5.055553s

String#gsub versus String#tr

def slow
  'slug from title'.gsub(' ', '_')
end

def fast
  'slug from title'.tr(' ', '_')
end
slow   187349.7 (±6.8%) i/s -     933140 in   5.012634s
fast  1216071.5 (±8.9%) i/s -    6050762 in   5.024810s

Parallel versus sequential assignment

def slow
  a, b = 1, 2
end

def fast
  a = 1
  b = 2
end
slow   189642.1 (±7.6%) i/s -     947520 in   5.031411s
fast  1180907.4 (±7.1%) i/s -    5872020 in   5.002410s

Explicit versus implicit String concatenation

def slow
  "foo" + "bar"
end

def fast
  "foo" "bar"
end
slow  1998832.8 (±5.5%) i/s -    9974259 in   5.005887s
fast  3559754.8 (±6.8%) i/s -   17747928 in   5.012536s

Using exceptions for control flow

def slow
  self.no_method
rescue NoMethodError
  "doh!"
end

def fast
  respond_to?(:no_method) ? self.no_method : "doh!"
end
slow   194665.7 (±10.9%) i/s -     963144 in   5.029578s
fast  2248844.0  (±6.1%) i/s -   11241017 in   5.020091s

while loops versus each_with_index

ARRAY = [1, 2, 3, 1, '2', 4, '5', 6, 7, 8, 9,'10']

def slow
  hash = {}

  ARRAY.each_with_index do |item, index|
    hash[index] = item
  end

  hash
end

def fast
  hash = {}
  index = 0
  length = ARRAY.length

  while index < length
    hash[index] = ARRAY[index]
    index += 1
  end

  hash
end
slow   147515.1 (±9.6%) i/s -     734100 in   5.038291s
fast   183634.1 (±6.5%) i/s -     918335 in   5.023060s

Related Pull Request for ActiveRecord: Perf: micro optimised Result column hash_row creation

:plus1:

2 Floor has deleted

好贴!挨个学习学习了!

好贴 :plus1:

值的一赞!

先赞一下,在拜读!!

看到几个让人惊异的地方,好贴!

实现者本来是打算让程序员不用关心这些细节的....

刚才看了一下,有几个小疑问 1、String#gsub versus String#sub 其实没有可比性 gsub 是全局替换,sub 只替换第一个,应用场景不一样,所以没有可比性。 2、疑问 为什么直接赋值 更快呢? 我的理解是 a,b = 1, 2 会产生一个新的 array[1, 2],然后在赋值给 a, b;所以为慢一点。@larrylv 你说呢?

Parallel versus sequential assignment

def slow
  a, b = 1, 2
end

def fast
  a = 1
  b = 2
end

3、 Explicit versus implicit String concatenation

def slow "foo" + "bar" end

def fast "foo" "bar" end

可以尝试其他的比较

require 'benchmark/ips'
def slow
    "foo" + "bar"
end

def fast
    "foo" "bar"
end

def append
  "foo" << "bar"
end

def another
  "%s%s" %["foo", "bar"]
end

Benchmark.ips do |x|
  x.report("slow") { slow { 1 + 1 } }
  x.report("fast") { fast { 1 + 1 } }
  x.report("append") { fast { 1 + 1 } }
  x.report("another") { fast { 1 + 1 } }
end

测试结果如下

Calculating -------------------------------------
                slow     37185 i/100ms
                fast     72146 i/100ms
              append     71189 i/100ms
             another     71661 i/100ms
-------------------------------------------------
                slow  1477945.1 (±10.8%) i/s -    7325445 in   5.012391s
                fast  3086600.4 (±7.8%) i/s -   15367098 in   5.012460s
              append  3086374.5 (±9.1%) i/s -   15305635 in   5.007838s
             another  3038806.7 (±9.9%) i/s -   15048810 in   5.010956s

发现'another'的的效率最高,而且最安全

@larrylv 第一次见 这中写法,能大致讲讲么? def fast "foo" "bar" end

#10 楼 @meeasyhappy

gsub 是全局替换,sub 只替换第一个,应用场景不一样,所以没有可比性。

不是没有可比性,而是说如果只需要替换第一个,就应该使用 sub 而不是 gsub,否则效率会下降。

为什么直接赋值 更快呢

嗯,其实这个和 Proc#call v.s. yield 的道理是一样的,会产生新的数组对象,所以更慢。

Explicit versus implicit String concatenation

这个问题实际上也同理。"a" + "b" 就会产生两个对象,然后相加后就会产生第三个对象,而使用 "a" "b" 只用创建一个对象。

fast   3086600.4 (±7.8%) i/s -   15367098 in   5.012460s
append  3086374.5 (±9.1%) i/s -   15305635 in   5.007838s
another  3038806.7 (±9.9%) i/s -   15048810 in   5.010956s

从你的结果来看,还是 fast 更快啊,只是这几种写法差异没那么大就是了。

其实关于 String 还有一个要注意的是,尽量避免使用 str += 'a' 这种写法,而是用 str << 'a',道理一样,避免创建多个对象。

👍 我补充一个 Hash#slice versus Hash#slice! 这个当初让我很意外

def slow
  hash = { a: 1, b: 2, c: 3, d: 4 }
  hash.slice!(:a, :x)
  hash
end

def fast
  { a: 1, b: 2, c: 3, d: 4 }.slice(:a, :x)
end
slow   157479.3 (±10.5%) i/s -     784800 in   5.064988s
fast   414745.2 (±6.4%) i/s -     2072673 in   5.024863s

#14 楼 @hbin Hash#slice 似乎是 ActiveSupport 中的 monkeypatch?

#15 楼 @larrylv 是的,active_support/core_ext/hash/slice.rb

这个一定要赞啊!! :plus1:

#12 楼 @larrylv 在给你一个赞。

看来基本都是慢在对象创建上

#19 楼 @palytoxin 是的。推荐 Sam Saffron 去年在 GoGaRuCo Conf 上的 talk,里面讲了一些 profiling 的技巧。https://www.youtube.com/watch?v=LWyEWUD-ztQ

创建对象就是一种开销,越少的创建临时对象对速度的提升肯定有好处,尤其是在遍历的时候

#21 楼 @sapronlee 嗯,创建对象是个立即的开销;而当创建对象过多,Ruby 的 GC 就会被 trigger,GC 运行一次要 50~100 ms...

23 Floor has deleted

loops each_with_index 差别不大,真要有这么大量循环的话,可以考虑别的实现了

最好的程序员知道每一行代码究竟干了什么。

#25 楼 @wujian_hit IR 实验室的?

Ruby Rogues 是怎么样的一个社区?

#28 楼 @dongbeta Ruby Rogues 是 Ruby 社区中非常有名的 Podcast, 每期会邀请一些嘉宾讨论技术相关的 Topic,Teahour 最初应该就是模仿 Ruby Rogues 做起来的。

Parley 最初是一个 private 的邮件列表 (Google Group),付费才能进入。也几乎都是关于 Ruby 的技术讨论,里面有很多 Ruby 社区的知名开发者,讨论的内容让我收获很多。后来由于 Discourse 的出现,Rogues 的成员决定弃用 mail-list 而采用论坛的形式进行技术讨论,每天都会有优质的帖子出现。

#29 楼 @larrylv 感谢介绍。我刚才付费加入。不知道几种付费 Plan 在内容的获取上有没有权限的区别?

32 Floor has deleted
33 Floor has deleted
34 Floor has deleted

def slow {:foo => :bar}.fetch(:foo, (1..10).to_a) end

def fast {:foo => :bar}.fetch(:foo) { (1..10).to_a } end

这个没测出区别 ruby 2.1.1 而且在有 foo 和没有 foo 之间差别太多了,这完全是误导人

更多的是同样的写法,在不同的情况下,造成的差别是非常大的,比如测试的数据

#35 楼 @xjz19901211 我和原帖作者都是 2.1.1 下测试的,结果还是有差别的。我觉得原因还是一样的,slow 方法中需要不断的创建 (1..10).to_a 导致性能上的开销。

#36 楼 @xjz19901211

更多的是同样的写法,在不同的情况下,造成的差别是非常大的,比如测试的数据

没太明白这句话的意思。

但是事实上确实是同样的写法,一种写法会过多的创造对象从而 trigger gc 导致耗时,另一种则不会。

赞。

ARRAY = [1, 2, 3, 1, '2', 4, '5', 6, 7, 8, 9,'10']

def slow
  hash = {}

  ARRAY.each_with_index do |item, index|
    hash[index] = item
  end

  hash
end

def fast
  hash = {}
  index = 0
  length = ARRAY.length

  while index < length
    hash[index] = ARRAY[index]
    index += 1
  end

  hash
end

这个例子是让人回到原始社会么

不错,收藏了。

#10 楼 @meeasyhappy #12 楼 @larrylv

你们都不认真看测试代码啊……函数调用怎么可能超过字面值的速度。。。后面 3 个全是 fast 了。。。

我的测试结果……

Calculating -------------------------------------
                slow    102405 i/100ms
                fast    124485 i/100ms
              append     99335 i/100ms
             another     51917 i/100ms
-------------------------------------------------
                slow  2750936.6 (±5.5%) i/s -   13722270 in   5.004513s
                fast  5119028.6 (±4.1%) i/s -   25643910 in   5.019501s
              append  2886743.5 (±4.2%) i/s -   14502910 in   5.032930s
             another   836874.4 (±4.8%) i/s -    4205277 in   5.036739s

#43 楼 @Kabie 我确实没有做 @meeasyhappy 提到的测试。

@Kabie , @larrylv 可以参见#10 楼 在我机器上测的结果,和你的差别挺大的 可能是每个机器的配置不一样的缘故。

#45 楼 @meeasyhappy ...你再仔细看看你的代码

Benchmark.ips do |x|
  x.report("slow") { slow { 1 + 1 } }
  x.report("fast") { fast { 1 + 1 } }
  x.report("append") { fast { 1 + 1 } }
  x.report("another") { fast { 1 + 1 } }
end

@Kabie 汗!!写错了....

mark 了~

You need to Sign in before reply, if you don't have an account, please Sign up first.