ruby 中的无限生成器速度 - 方法 vs 枚举器 vs 惰性
Infinite generator speed in ruby - Method vs Enumerator vs Lazy
警告:这个实用价值不大。我只想知道这是怎么回事。
我在网上多次看到这条线:
return to_enum __method__ unless block_given?
我想测试它并用它制作一个生成器方法(#1),然后我尝试不使用它并想出了一个枚举器(#2)。我对此很满意,认为这就是应该做的方式 - 尽管后来我想出了一个使用 #lazy (#3) 的解决方案,我认为它看起来更优雅。
你能猜出哪个最快吗?令我惊讶的是,#1 是最快的,其次是#2 和#3!
对我来说,第一个看起来有点 hack 并且有不良行为,例如如果你给它一个空块就会停止(而不是如果给定一个块就抛出错误)。
我的问题是,最快的方法是什么?
为什么#1 比#2 快,我错过了什么?如果我的解决方案没问题,客观上哪个是最好的?
编辑:更简单的示例,之前是 fizzbuzz (http://pastebin.com/kXbbfxBc)
def method
return to_enum __method__ unless block_given?
n = 0
loop do
n += 1
yield n ** 2
end
end
enum = Enumerator.new do |yielder|
n = 0
loop do
n += 1
yielder.yield n ** 2
end
end
lazy = (1..Float::INFINITY).lazy.map do |n|
n ** 2
end
p method.take 50
p enum.take 50
p lazy.first 50
require 'benchmark/ips'
Benchmark.ips do |bm|
bm.report('Method'){ method.take 50 }
bm.report('Enumerator'){ enum.take 50 }
bm.report('Lazy'){ lazy.first 50 }
bm.compare!
end
后两种形式都有块绑定,这确实有一些开销;当您使用块创建枚举器时,Ruby 会将块转换为 Proc 并将其分配给 Enumerator::Generator
,然后通过调用 proc 进行迭代。这有直接调用方法没有的开销。
如果我们消除块形式,性能损失也会消除:
def method
return to_enum __method__ unless block_given?
n = 0
loop do
n += 1
yield n ** 2
end
end
def method_sans_enum
n = 0
loop do
n += 1
yield n ** 2
end
end
method_enum = Enumerator.new(self, :method_sans_enum)
enum = Enumerator.new do |yielder|
n = 0
loop do
n += 1
yielder.yield n ** 2
end
end
lazy = (1..Float::INFINITY).lazy.map do |n|
n ** 2
end
p method.take 50
p enum.take 50
p method_enum.take 50
p lazy.first 50
require 'benchmark/ips'
Benchmark.ips do |bm|
bm.report('Method'){ method.take 50 }
bm.report('Enumerator'){ enum.take 50 }
bm.report('Enumerator 2'){ method_enum.take 50 }
bm.report('Lazy'){ lazy.first 50 }
bm.compare!
end
结果:
Method 10.874k i/100ms
Enumerator 6.152k i/100ms
Enumerator 2 11.733k i/100ms
Lazy 3.885k i/100ms
Comparison:
Enumerator 2: 132050.2 i/s
Method: 124784.1 i/s - 1.06x slower
Enumerator: 65961.9 i/s - 2.00x slower
Lazy: 40063.6 i/s - 3.30x slower
调用过程涉及调用方法没有的开销;例如:
class Foo
def meth; end
end
instance = Foo.new
pr = instance.method(:meth).to_proc
require 'benchmark/ips'
Benchmark.ips do |bm|
bm.report('Method'){ instance.meth }
bm.report('Proc'){ pr.call }
bm.compare!
end
结果:
Calculating -------------------------------------
Method 121.016k i/100ms
Proc 104.612k i/100ms
-------------------------------------------------
Method 6.823M (± 0.1%) i/s - 34.127M
Proc 3.443M (± 6.4%) i/s - 17.156M
Comparison:
Method: 6822666.0 i/s
Proc: 3442578.2 i/s - 1.98x slower
调用已转换为 proc 的方法比直接调用该方法慢 2 倍 - 几乎与您观察到的性能偏差完全一样。
警告:这个实用价值不大。我只想知道这是怎么回事。
我在网上多次看到这条线:
return to_enum __method__ unless block_given?
我想测试它并用它制作一个生成器方法(#1),然后我尝试不使用它并想出了一个枚举器(#2)。我对此很满意,认为这就是应该做的方式 - 尽管后来我想出了一个使用 #lazy (#3) 的解决方案,我认为它看起来更优雅。
你能猜出哪个最快吗?令我惊讶的是,#1 是最快的,其次是#2 和#3!
对我来说,第一个看起来有点 hack 并且有不良行为,例如如果你给它一个空块就会停止(而不是如果给定一个块就抛出错误)。
我的问题是,最快的方法是什么? 为什么#1 比#2 快,我错过了什么?如果我的解决方案没问题,客观上哪个是最好的?
编辑:更简单的示例,之前是 fizzbuzz (http://pastebin.com/kXbbfxBc)
def method
return to_enum __method__ unless block_given?
n = 0
loop do
n += 1
yield n ** 2
end
end
enum = Enumerator.new do |yielder|
n = 0
loop do
n += 1
yielder.yield n ** 2
end
end
lazy = (1..Float::INFINITY).lazy.map do |n|
n ** 2
end
p method.take 50
p enum.take 50
p lazy.first 50
require 'benchmark/ips'
Benchmark.ips do |bm|
bm.report('Method'){ method.take 50 }
bm.report('Enumerator'){ enum.take 50 }
bm.report('Lazy'){ lazy.first 50 }
bm.compare!
end
后两种形式都有块绑定,这确实有一些开销;当您使用块创建枚举器时,Ruby 会将块转换为 Proc 并将其分配给 Enumerator::Generator
,然后通过调用 proc 进行迭代。这有直接调用方法没有的开销。
如果我们消除块形式,性能损失也会消除:
def method
return to_enum __method__ unless block_given?
n = 0
loop do
n += 1
yield n ** 2
end
end
def method_sans_enum
n = 0
loop do
n += 1
yield n ** 2
end
end
method_enum = Enumerator.new(self, :method_sans_enum)
enum = Enumerator.new do |yielder|
n = 0
loop do
n += 1
yielder.yield n ** 2
end
end
lazy = (1..Float::INFINITY).lazy.map do |n|
n ** 2
end
p method.take 50
p enum.take 50
p method_enum.take 50
p lazy.first 50
require 'benchmark/ips'
Benchmark.ips do |bm|
bm.report('Method'){ method.take 50 }
bm.report('Enumerator'){ enum.take 50 }
bm.report('Enumerator 2'){ method_enum.take 50 }
bm.report('Lazy'){ lazy.first 50 }
bm.compare!
end
结果:
Method 10.874k i/100ms
Enumerator 6.152k i/100ms
Enumerator 2 11.733k i/100ms
Lazy 3.885k i/100ms
Comparison:
Enumerator 2: 132050.2 i/s
Method: 124784.1 i/s - 1.06x slower
Enumerator: 65961.9 i/s - 2.00x slower
Lazy: 40063.6 i/s - 3.30x slower
调用过程涉及调用方法没有的开销;例如:
class Foo
def meth; end
end
instance = Foo.new
pr = instance.method(:meth).to_proc
require 'benchmark/ips'
Benchmark.ips do |bm|
bm.report('Method'){ instance.meth }
bm.report('Proc'){ pr.call }
bm.compare!
end
结果:
Calculating -------------------------------------
Method 121.016k i/100ms
Proc 104.612k i/100ms
-------------------------------------------------
Method 6.823M (± 0.1%) i/s - 34.127M
Proc 3.443M (± 6.4%) i/s - 17.156M
Comparison:
Method: 6822666.0 i/s
Proc: 3442578.2 i/s - 1.98x slower
调用已转换为 proc 的方法比直接调用该方法慢 2 倍 - 几乎与您观察到的性能偏差完全一样。