【问题标题】:Infinite generator speed in ruby - Method vs Enumerator vs Lazyruby 中的无限生成器速度 - 方法 vs 枚举器 vs 懒惰
【发布时间】:2015-02-12 11:33:47
【问题描述】:

警告:这没什么实用价值。我只是想知道发生了什么事。

我在网上多次遇到过这条线:

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

【问题讨论】:

  • 请将您的问题放在问题中,而不是在某个随机网站上发布最重要的部分。
  • @JörgWMittag 粘贴箱?几乎不是一个随机的网站,它有 68 行 - 确实太多了,无法提出问题。如果你指的是标题,我在措辞上很吃力,我不介意你编辑它。
  • @user21033168 也许你可以想出一个更短的例子。我认为您的问题与 fizz-buzz 算法无关,是吗?
  • @Stefan 好的,谢谢。

标签: ruby performance enumerator lazy-sequences


【解决方案1】:

后两种形式都有块绑定,确实有一些开销;当您使用块创建 Enumerator 时,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

调用 procs 涉及调用方法所没有的开销;例如:

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 倍 - 几乎与您观察到的性能偏差完全相同。

【讨论】:

  • 很好,不仅速度更快,而且表现得像一个普通的枚举。我使用method_enum = to_enum :method_sans_enum 来避免警告。有趣的是,这是最快的方式,谢谢。
  • 我还发现while trueloop do 快得多!
猜你喜欢
  • 1970-01-01
  • 2013-09-22
  • 1970-01-01
  • 1970-01-01
  • 2011-03-24
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-12-24
相关资源
最近更新 更多