为什么在某些情况下,Ruby 的 Hash#values 比 Hash#each_value 快?



当我将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 倍。 为什么会这样呢?

迭代

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 .)

要回答"为什么会这样"这个问题,人们可能应该看看不同 ruby 版本的rb_hash_foreachrb_array_foreach原生实现。

我会

说原因是优化了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
没什么奇怪的,

只是要知道,在大多数情况下,这是您不应该依赖的特定于实现的细节。算法的复杂性才是最重要的。

最新更新