ruby - 为什么#map 比#each 更有效?

标签 ruby performance

当你只有一把锤子时,一切看起来都像钉子。因此,在发现 Array#mapArray#select< 的实用性、优雅性和语法乐趣之前,可以说是 Ruby 中的 Array#each 方法 和其他可迭代方法。我很好奇的是:

为什么在使用更精确的可迭代方法时性能会实际提高?一般来说,这是真的吗?

例如,在

require 'benchmark'

array = (1..100000).to_a

puts Benchmark.measure {
  100.times do
    array.map { |el| el.even? }
  end
}

puts Benchmark.measure {
  100.times do
    new_array = []
    array.each do |el| 
      new_array << el.even? 
    end
  end
}

# ruby bench.rb
# 0.450598   0.015524   0.466122 (  0.466802)
# 0.496796   0.018525   0.515321 (  0.516196)

Benchmark 始终显示有利于 Array#map 的时间性能差异。在以下代码中:

puts Benchmark.measure {
  100.times do
    array.select { |el| el.even? }
  end
}

puts Benchmark.measure {
  100.times do
    new_array = []
    array.each do |el| 
      if el.even? 
         new_array << el
      end
    end
  end
}

# ruby bench.rb
# 0.405254   0.007965   0.413219 (  0.413733)
# 0.471416   0.008875   0.480291 (  0.481079)

Array#select 每次都击败了一个偷工减料的 Array#each

那么为什么这些更精确的方法会产生明显更好的性能呢?这是 Ruby 和/或所有语言中的通用公理吗?

最佳答案

在您的两个示例中,第二段代码分配的内存是第一段代码的 100 倍。它还对数组执行大约 log_1.5(100) 次调整大小(假设动态数组的标准教科书实现具有 1.5 的增长因子)。调整数组的大小是昂贵的(分配一个新的内存块,然后将所有元素的 O(n) 副本复制到新的内存块中)。更一般地说,垃圾收集器讨厌突变,它们收集大量生命周期短的小对象比保持一些生命周期长的大对象更有效。

换句话说,在第一个示例中,您正在测量 Array#mapArray#select ,分别,而在第二个例子中,你不仅测量 Array#each , 还有 Array#<<以及数组大小调整和内存分配。从基准测试结果中无法判断哪些贡献了多少。正如 Zed Shaw 曾经说过的那样:"If you want to measure something, then don't measure other shit" .

但即使您在基准测试中修复了该错误,一般来说,更专业的操作比通用操作具有更多可用信息,因此更通用的操作通常不会比专业操作更快。

在您的特定示例中,它可能只是一些非常简单的事情,例如,您使用的 Ruby 实现不太擅长优化 Ruby 代码(例如 YARV,与 TruffleRuby 不同),同时具有优化的Array#map 的 native 实现和 Array#select (再次以 YARV 为例,它对这两者都有 C 实现,并且通常不能很好地优化 Ruby 代码)。

最后,编写正确的微基准测试很困难。真的,真的,真的很难。我鼓励阅读并理解 mechanical-sympathy 上的整个讨论主题。邮寄名单:JMH vs Caliper: reference thread .虽然它专门针对 Java 基准测试(实际上是关于 JVM 基准测试),但许多论点适用于任何现代高性能 OO 执行引擎,例如 Rubinius、TruffleRuby 等。在较小程度上也适用于 YARV。请注意,大部分讨论都是关于编写微基准测试工具,而不是编写微基准测试本身,即它是关于编写允许开发人员编写正确的微基准测试的框架,不必了解那些东西,但不幸的是,即使有最好的微基准测试工具(Ruby 的 Benchmark 实际上不是一个很好的工具),您仍然需要对现代编译器、垃圾收集器、执行引擎、CPU、硬件架构有非常深刻的理解, 还有统计数据。

这是一个失败的基准测试的好例子,对于未受过训练的基准测试编写者来说可能并不明显:Why is printing “B” dramatically slower than printing “#”? .

关于ruby - 为什么#map 比#each 更有效?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/60011859/

相关文章:

ruby-on-rails - 为什么我在使用虚线域名 (example-dashed.com) 时会丢失 session ?

ruby-on-rails - 使用 cookie 制作 Ruby Net::HTTP::Get 请求

javascript - javascript 加载速度慢,代码看起来很大?

sql - 更新大型表上的行的最高效方法

performance - 三角函数的效率/速度

ruby - 强制所有 http 请求遵循系统代理配置

html - rails 将 html 转换为图像

sql - Entity Framework select可以阻塞表吗?

.NET 线程处于 'pre-emptive GC disabled' 模式,会阻塞 GC 并可能导致死锁

javascript - 如何将 nil 序列化为 nil,而不是空的地方?