为啥#map 比#each 更有效?
Posted
技术标签:
【中文标题】为啥#map 比#each 更有效?【英文标题】:Why is #map more efficient than #each?为什么#map 比#each 更有效? 【发布时间】:2020-05-17 14:14:49 【问题描述】:当你只有一把锤子时,一切看起来都像钉子。所以可以说 Ruby 中的Array#each
方法,在发现Array#map
和Array#select
和其他可迭代方法的实用性、优雅性和语法乐趣之前。我很好奇的是:
为什么在使用更精确的可迭代方法时性能会有所提高?一般情况下是这样吗?
例如,在
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 和/或所有语言中的通用公理吗?
【问题讨论】:
FWIW,在您的第二个示例中,new_array
将比 map 返回的数组大 100 倍,并在您的基准测试完成运行时选择,因为它不会在运行。不知道这是否会导致性能差异,但您可能需要检查一下。
我认为我们可以得出结论,专用方法总是比以特定方式使用的更通用方法更快(或至少不慢),原因很简单,前者是对后者进行封装,Ruby 的核心方法编写者努力优化性能。我想有人可能会争辩说,出于内存考虑,某些核心方法可能不会针对速度进行优化,但它们仍然会针对某些性能指标进行优化,因此在相同的指标上,它们不会比经过调整的通用方法更差。
不应该将new_array = []
放在100.times
块内以获得相同的结果吗?您目前正在比较 2 个不同的任务。
天啊!感谢您的提醒。固定!
【参考方案1】:
在这两个例子的第二种情况下,每次迭代都有一个赋值。第一个没有分配任何东西。
【讨论】:
这是一个非常省力的答案。请参阅其他答案以获取良好答案的示例。【参考方案2】:在您的两个示例中,第二段代码分配的内存是第一段代码的 100 倍。它还执行大约 log_1.5(100) 的数组大小调整(假设标准教科书实现了增长因子为 1.5 的动态数组)。调整数组的大小是昂贵的(分配一个新的内存块,然后将所有元素的 O(n) 复制到新的内存块中)。更一般地说,垃圾收集器讨厌突变,它们在收集大量短寿命的小对象方面比保持一些大的长寿命对象更有效。
换句话说,在第一个示例中,您分别测量Array#map
和Array#select
,而在第二个示例中,您不仅测量Array#each
,还测量Array#<<
以及数组大小调整和内存分配。从基准测试结果中无法判断哪些贡献了多少。正如 Zed Shaw 所说:"If you want to measure something, then don't measure other shit"。
但即使您在基准测试中修复了该错误,一般而言,更专业的操作比一般操作拥有更多可用信息,因此更一般的操作通常不会比专业操作更快。
在您的具体示例中,它可能只是一些非常简单的事情,例如,您使用的 Ruby 实现不太擅长优化 Ruby 代码(例如 YARV,与 TruffleRuby 不同),同时具有优化的Array#map
和 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 “#”?。
【讨论】:
【参考方案3】:在分析任何算法时,我们主要考虑时间复杂度和空间复杂度。在分析不同算法来解决特定任务之前,首要的事情是设计执行相同任务并返回相同期望输出的不同算法。
让我们编写一个执行相同任务的程序(遍历数组 100 次。仅此而已。) 不存储任何结果(因为我不确定什么样的输出你想要)
这是 bench.rb 文件的代码 sn-p
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
array.each |el| el.even?
end
puts Benchmark.measure
100.times do
array.select |el| el.even?
end
这段代码我已经运行了3次,结果如下:
Output:
Attempt 1:
0.548562 0.021844 0.570406 ( 0.571088)
0.457079 0.000345 0.457424 ( 0.457774)
0.516487 0.010758 0.527245 ( 0.527843)
Attempt 2:
0.544863 0.021756 0.566619 ( 0.568487)
0.458062 0.000514 0.458576 ( 0.459249)
0.508665 0.010847 0.519512 ( 0.520401)
Attempt 3:
0.583084 0.022554 0.605638 ( 0.606023)
0.509447 0.000665 0.510112 ( 0.511088)
0.548483 0.012212 0.560695 ( 0.561534)
根据书面示例,我可以看到Array#each
是明显的赢家。输出可能会根据您的要求而有所不同,但基本规则应该相同,即算法应该返回相同的所需输出。
【讨论】:
“让我们编写一个执行相同任务的程序” 在您的示例中,给出的每段代码都执行不同的任务。array.map |el| el.even?
将检查数字是否为偶数并输出具有 true
和 false
值的新数组(大小为 100,000)。 array.each |el| el.even?
将检查数字是否为偶数,仅此而已。 array.select |el| el.even?
将检查数字是否为偶数并输出一个只有偶数的新数组(大小为 50,000)。这些不是完全一样的任务。
是的。这是正确的,它定义了each
、map
和select
之间的区别。在内部,它们的工作方式不同,这就是它们的时间复杂度也会有所不同的原因。我只是想用相同的逻辑(el.even?
)执行循环,而不是在任何地方存储结果或根据输出执行任何进一步的逻辑。根据需求,这些方法应运而生。以上是关于为啥#map 比#each 更有效?的主要内容,如果未能解决你的问题,请参考以下文章