Ruby的Map/Reduce函数一定高效吗?

3
b1 = Time.now
puts (1..100000).inject(0) { |x, y| x + y }
a1 = Time.now
puts "Time for inject: #{a1 - b1}"

b2 = Time.now
sum = 0
(1..100000).each do |value|
    sum += value
end
puts sum
a2 = Time.now
puts "Time for each: #{a2 - b2}"

上面的Ruby代码比较了两种整数求和的方法。令人惊讶的是,更优雅的“inject”或“reduce”方法被另一种方法性能超越。为什么会这样?为什么人们要费劲地使用低效的“inject”或“reduce”呢?仅仅因为它更优雅吗?
PS:感谢所有激励人心的答案。我的意图是询问背后发生了什么导致了差异。

1
如果你在做任何事情时都计算纳秒,那么你就没有时间做任何有生产力的事情。同时请参考“过早优化”(不要使用缩写版本,因为完整的引用正确地指出,在某些罕见情况下,这样的事情确实很重要)。 - user395760
谢谢。非常周到的建议! - Terry Li
6个回答

6
我认为在这种情况下需要进行一些数学计算:

require "benchmark"

N = 5_000_000

Benchmark.bmbm do |bm|
  bm.report "inject 1" do
    (1..N).inject(0) { |x, y| x + y }
  end

  bm.report "inject 2" do
    (1..N).inject(:+)
  end

  bm.report "each" do
    sum = 0
    (1..N).each do |value|
      sum += value
    end
  end

  bm.report "sum of finite arithmetic progression" do
    ((1 + N) * N) / 2
  end
end

结果如下:

% ruby sum.rb
Rehearsal ------------------------------------------------------------------------
inject 1                               0.500000   0.000000   0.500000 (  0.507497)
inject 2                               0.320000   0.000000   0.320000 (  0.322675)
each                                   0.370000   0.000000   0.370000 (  0.380504)
sum of finite arithmetic progression   0.000000   0.000000   0.000000 (  0.000005)
--------------------------------------------------------------- total: 1.190000sec

                                           user     system      total        real
inject 1                               0.500000   0.000000   0.500000 (  0.507697)
inject 2                               0.320000   0.000000   0.320000 (  0.322323)
each                                   0.370000   0.000000   0.370000 (  0.380307)
sum of finite arithmetic progression   0.000000   0.000000   0.000000 (  0.000004)
% 

更好的数学总是更快的 :)

1
最后一个非常特别。真是一种灵感!谢谢。 - Terry Li

5

是的,代码可读性比微小优化更重要。即使对数百万个元素进行求和,它们之间的差异也几乎不可察觉。而且,两种方法都是O(n),因此随着元素数量的增加,它们都不会显著地优于另一种。

正如其他人指出的那样,inject(:+)仍然稍微快一点。即使不是这样,也要选择最容易理解的方式,不要过分担心性能上的微小差异。这可能不是您应用程序的瓶颈。

require "benchmark"

N = 5_000_000

Benchmark.bmbm do |bm|
  bm.report "inject 1" do
    (1..N).inject(0) { |x, y| x + y }
  end

  bm.report "inject 2" do
    (1..N).inject(:+)
  end

  bm.report "each" do
    sum = 0
    (1..N).each do |value|
      sum += value
    end
  end
end

结果:

               user     system      total        real
inject 1   0.610000   0.000000   0.610000 (  0.613080)
inject 2   0.370000   0.000000   0.370000 (  0.370892)
each       0.570000   0.000000   0.570000 (  0.568266)

1
胜利者是...(1+N)*N/2 - steenslag
1
@steenslag,不错!但是如果Range#each被重新定义了,它会惨败的!;) - molf

3
请尝试以下方法:
puts (1..100000).inject(:+)

个人而言,我更喜欢简洁优雅的代码。如果一行代码可以代替三行代码而且不会让代码变得混乱,我会选择使用 inject。


谢谢。我刚试了一下这个。情况变得更有趣了:这个比我的两个都表现得更好!那里发生了什么? - Terry Li
一个代表方法 + 的符号被发送并应用于元素之间。例如,使用 map 也可以做类似的事情,['a', 'b', 'c'].map(&:upcase) 将返回 ['A', 'B', 'C']。另外一个我没有提到的点是,当你不传递一个起始值给 inject 时,它会使用第一个元素,所以在这种情况下,().inject(0){} 中的 0 是不必要的。 - derp

3

@derp是正确的。下次建议您使用基准测试模块,例如:

#!/usr/bin/env ruby

require "benchmark"

Benchmark.bm do |x|
  x.report { (1..10000000).inject(:+) }
  x.report { sum = 0; (1..10000000).each { |value| sum += value } }
end

谢谢。我不知道基准模块,它似乎非常方便用于代码分析。 - Terry Li

2

有趣的是,之前提供的回答中大多数或全部都假设使用的是最新主要版本的Ruby(1.9)。在1.8.7版本中这种差异更加明显:

$ ruby -v
ruby 1.8.7 (2011-02-18 patchlevel 334) [i686-darwin10.6.0], MBARI 0x6770, Ruby Enterprise Edition 2011.03
$ ruby bench.rb 

Rehearsal ------------------------------------------------------------------------
inject 1                               3.910000   0.010000   3.920000 (  3.932388)
inject 2                               0.660000   0.000000   0.660000 (  0.662330)
each                                   1.120000   0.010000   1.130000 (  1.126276)
sum of finite arithmetic progression   0.000000   0.000000   0.000000 (  0.000009)
--------------------------------------------------------------- total: 5.710000sec

                                           user     system      total        real
inject 1                               3.930000   0.010000   3.940000 (  3.956084)
inject 2                               0.680000   0.000000   0.680000 (  0.685073)
each                                   1.110000   0.000000   1.110000 (  1.109675)
sum of finite arithmetic progression   0.000000   0.000000   0.000000 (  0.000009)

完全同意易读性和维护性更为重要。


1

Ruby并不是关于性能的。如果你想要性能,还有其他专门设计用于此目的的语言。

Ruby的全部意义在于写出优雅的代码带来的愉悦感 :)


是的。所以 Matz 只是出于兴趣发明了 Ruby 吗? - Terry Li
1
@TerryLiYifeng 我想是这样的 :) Ruby 很适合 web 开发,因为它非常方便、优雅和灵活。虽然它是一种快速解释型语言,但快并不是它的特点之一。 - Nicolas Guillaume

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接