【问题标题】:Why is Ruby's Hash#values faster than Hash#each_value in certain cases?为什么在某些情况下 Ruby 的 Hash#values 比 Hash#each_value 快?
【发布时间】:2016-06-23 10:44:12
【问题描述】:

当我将each_value 应用于哈希时,它所用的时间比我使用values 时要长得多,尽管each_value 表面上避免了分配和复制数组。

我写了一个简单的对比:

require 'benchmark/ips'

some_hash = File.open('with_an.dat') { |f| Marshal.load f }

Benchmark.ips do |x|
  x.report "calling each_value" do
    some_hash.each_value
  end
  x.report "calling values" do
    some_hash.values
  end
  x.compare!
end

Benchmark.ips do |x|
  x.report "summing each_value" do
    some_hash.each_value.inject &:+
  end
  x.report "summing values" do
    some_hash.values.inject &:+
  end
  x.compare!
end

结果如下:

Calculating -------------------------------------
  calling each_value    58.166k i/100ms
      calling values     2.000  i/100ms
-------------------------------------------------
  calling each_value      1.312M (±40.7%) i/s -      5.468M
      calling values     29.423  (±10.2%) i/s -    146.000 

Comparison:
  calling each_value:  1312156.6 i/s
      calling values:       29.4 i/s - 44596.28x slower

Calculating -------------------------------------
  summing each_value     1.000  i/100ms
      summing values     1.000  i/100ms
-------------------------------------------------
  summing each_value      2.107  (± 0.0%) i/s -     11.000 
      summing values      8.002  (±12.5%) i/s -     40.000 

Comparison:
      summing values:        8.0 i/s
  summing each_value:        2.1 i/s - 3.80x slower

正如预期的那样,仅仅调用每个方法,each_value 的速度要快得多,因为它只需要创建一个Enumerator,并且实际上并不遍历哈希表。同时,values 必须复制整个数组。

然而,当我将这些值相加时,each_value 方法似乎比values 方法慢 3 倍。为什么会这样?

【问题讨论】:

  • 产生每个单独值的开销?
  • inject(:+) 可以,而且应该比inject(&:+) 快。
  • @SergioTulentsev 你是什么意思?这两种方法都产生了单独的值。
  • @mudasobwa:啊,确实。

标签: ruby performance internals


【解决方案1】:

迭代Hash 比迭代Array 慢:

 ▶ Benchmark.bm do |x|
 ▷   x.report do
 ▷     n.times do
 ▷       {a: 1, b: 2, c: 3, d: 4, e: 5}.inject(1) { |memo, (_, v)| memo * v }
 ▷     end
 ▷   end
 ▷   x.report do
 ▷     n.times do
 ▷       [1, 2, 3, 4, 5].inject(1) { |memo, v| memo * v }
 ▷     end
 ▷   end
 ▷ end

 #⇒      user     system      total        real
 #⇒  0.700000   0.010000   0.710000 (  0.712821)
 #⇒  0.340000   0.000000   0.340000 (  0.349040)

通过调用each_value,实际上迭代了原始Hash 实例,而通过调用values.each,迭代正在Array 实例上完成(values。)

要回答“为什么会这样”这个问题,可能应该看看 rb_hash_foreachrb_array_foreach 不同 ruby​​ 版本的本机实现。

【讨论】:

  • 对,但是 Hash#values 必须遍历哈希才能生成数组结果,因此其成本将是生成数组(从而遍历哈希)加上遍历数组的成本。
  • 我不确定,但我有点看到哈希作为数组存储在内存中,键指向该数组的元素内存地址。
  • 后者没有解释为什么我们不能在需要的时候对each_value使用快速迭代器。我很欣赏这个问题:它相当棒,可能应该发给 Matz(说明为“为什么我们不能总是使用最快的迭代器?”。)
  • 那么Ruby肯定有改进的空间,但答案是对的。迭代 Array 比 Hash 更快,并且显然必须获取值数组(来源 here)并不能抵消性能差异。
【解决方案2】:

我会说原因是Hash#values方法实现的优化。

在您的第一个基准测试中,您将苹果(创建枚举器)与橙子(创建数组)进行比较。可以预料,构建整个数组的成本要比生成单个生成器的成本更高,该生成器需要额外调用才能访问最终值。

如果你写等价的例子,结果会有所不同:

  some_hash =  ('aa'..'zz').each_with_index.to_h

  Benchmark.ips do |x|
    x.report "array from map" do
      some_hash.map &:last
    end
    x.report "array from each_value" do
      some_hash.each_value.to_a
    end
    x.report "array from values" do
      some_hash.values
    end
    x.compare!
  end

  Comparison:
        array from values:   171143.8 i/s
    array from each_value:    15195.8 i/s - 11.26x slower
           array from map:     6040.9 i/s - 28.33x slower

没什么太令人惊讶的,请注意这是一个在大多数情况下不应该依赖的特定于实现的细节。算法复杂性才是最重要的。

【讨论】:

  • > 算法复杂性才是最重要的。没错,但问题的重点是 Ruby 内部结构,而不是算法设计。
  • @bcc32 那么答案是否满足了您的好奇心? Hash#valuesspecialized 方法,预期性能优于 generic 枚举器并随后获取其值。 Enumerable#inject 不能神奇地向后更改目标对象的创建方式,除非通过任何 Ruby 实现 AFAIK 中不存在的一些复杂的优化器。顺便提一句。在 JRuby 9.0.5.0 "summing each_value" 示例中仅慢 1.1 倍。
  • Hash#valuesspecialized 方法”的声明对于“Hash#values 方法中的 specialized 是什么”这个问题的回答有点糟糕: )
  • @mudasobwa 嗯?我没有看到 OP 引用的问题。 specialized 是指创建一个容器并在一个步骤/调用中存储所有结果,这可以更直接地进行优化,然后生成一个生成器,随后从中产生结果。
猜你喜欢
  • 2017-10-12
  • 1970-01-01
  • 2012-05-12
  • 1970-01-01
  • 2013-03-08
  • 2019-12-16
  • 1970-01-01
  • 2011-08-01
相关资源
最近更新 更多