基于我之前的相关question,我发现在使用Enumerator
类生成无限序列时存在巨大的性能差距。
在我确实认为问题与Enumerable
方法take
和drop
有关之前,但以下基准并未证实此声明。
创建自然数字生成器并在间隔中显示数字的示例(10e7-10,10e7>:
require 'benchmark'
nats_range = (1..Float::INFINITY)
nats_enum = Enumerator.new {|y| i=1; loop { y << i; i+=1 }}
puts "#{'_'*79+"\n"}Benchmarking Enumerable methods on Range ..."
puts Benchmark.measure { print nats_range.take(10**7).drop(10**7-10), "\n" }
puts "#{'_'*79+"\n"}Benchmarking Enumerable methods on Enumerator ..."
puts Benchmark.measure { print nats_enum.take(10**7).drop(10**7-10), "\n" }
$ ruby a.rb
_______________________________________________________________________________
Benchmarking Enumerable methods on Range ...
[9999991, 9999992, 9999993, 9999994, 9999995, 9999996, 9999997, 9999998, 9999999, 10000000]
1.570000 0.010000 1.580000 ( 1.576761)
_______________________________________________________________________________
Benchmarking Enumerable methods on Enumerator ...
[9999991, 9999992, 9999993, 9999994, 9999995, 9999996, 9999997, 9999998, 9999999, 10000000]
15.620000 0.020000 15.640000 ( 15.665156)
使用Enumerator的等效代码慢10倍!
我在这里问是否有人可以解释这个巨大的差异。我是否使用了不正确的枚举器?这是当前Ruby实现中的已知回归吗?
MRI Ruby 1.9.3p385
答案 0 :(得分:3)
Enumerator
基于Fiber
s,您可以将其视为非常轻量级的线程。 (实际上,它们是协程。)
Range
使用succ
进行迭代,使用<=
来确定它是否已到达终点。
因此,您的Range
示例使用了对Fixnum#succ
和Fixnum#<=
的2000万次方法调用,这两种方法都经过了大量优化,并且实际上或多或少地直接映射到对应的汇编指令。< / p>
您的Enumerator
示例使用了2000万次调用Enumerator::Yielder#<<
(谁知道它有多贵)和Fixnum#+
以及 1000万{{1}上下文切换。我可以很容易地想象Fiber
上下文切换比简单Fiber
操作贵10倍。
答案 1 :(得分:0)
我认为你没有问正确的问题。
如果您需要10个元素并且正在生成并存储100亿个元素,那么您的算法可能首先出现了问题。
此外,将Enumerator
与Range
进行比较是没有意义的,因为Range
无法为您提供所寻求的答案。
运行微基准测试很有趣,但请记住,它们通常无意义。无论如何,这是我得到的更合理的限制。
class AllNumbers
include Enumerable
def each
i = 0
loop { yield i += 1 }
end
end
custom = AllNumbers.new
enum = Enumerator.new do |y|
i=0
loop { y << i+=1 }
end
range = 1..Float::INFINITY
require 'fruity'
limit = 1000
compare do
using_range { range.take(limit) }
using_enumerator { enum.take(limit) }
using_custom_class { custom.take(limit) }
end
结果让我感到有些惊讶:
using_custom_class is faster than using_range by 20.0% ± 10.0%
using_range is faster than using_enumerator by 70.0% ± 10.0%
我不会猜到这一点。实际上,Range#each
未针对整数进行优化,而+=
已经过优化。