Scala Tuple2Zipped vs IterableLike zip
Posted
技术标签:
【中文标题】Scala Tuple2Zipped vs IterableLike zip【英文标题】: 【发布时间】:2019-06-09 16:02:00 【问题描述】:这两种实现有什么区别?这个比那个好吗。 有一篇博客文章说 Tuple2Zipped 性能更好,但没有提供原因,并且查看源代码我看不出有什么区别。
val l1 = List(1,2,3)
val l2 = List(5,6,7)
val v1 = l1 zip l2
val v2 = (l1, l2).zipped
【问题讨论】:
【参考方案1】:如果不明显,v1
和 v2
的值和类型不同:v1
的类型为 List[(Int, Int)]
,值为 List((1, 5), (2, 6), (3, 7))
; v2
的类型为 scala.runtime.Tuple2Zipped[Int, List[Int], Int, List[Int]]
,值为 (List(1, 2, 3), List(5, 6, 7)).zipped
。
换句话说,v1
的值是严格计算的(zip
操作已经完成),而v2
的计算是惰性(或非严格意义上的)——实际上,zip 操作已被存储,但尚未执行。
如果您只想计算这两个值(但不实际使用它们),那么我确实希望v2
的计算速度更快,因为它实际上并没有做很多工作。 ;-)
除此之外,这将取决于您随后打算如何使用这些值。如果您不需要处理结果列表中的每个元组,Tuple2Zipped
的性能会更好,因为它不会浪费时间压缩您不需要的列表元素。如果您需要对每个元组应用一些操作,但不需要在后处理中访问它们,那么它可能具有优势,因此只需通过列表一次。
List.zip
方法可能是更好的选择,如果您需要对列表成员执行多个操作并多次迭代。
这两种方法都适用于所有情况。 (在一般情况下,我更喜欢List.zip
,如果只是因为Tuple2Zipped
不太为人所知,并且它的使用会暗示特殊要求。)
如果性能确实是一个问题,那么我建议使用您的代码对这两种方法进行基准测试,使用ScalaMeter 之类的工具并准确区分两者。我还建议对内存使用情况和处理时间进行基准测试,因为这两种方法的内存要求不同。
更新:在下面的 cmets 中引用附加问题:“val m:Map[Int, Int] = (l1 zip l2)(breakOut)
和 (l1, l2).zipped.toMap
之间有区别吗?
我将重述如下:
import scala.collection.breakOut
val l1 = List(1, 2, 3)
val l2 = List(5, 6, 7)
// m1's type has to be explicit, otherwise it is inferred to be
// scala.collection.immutable.IndexedSeq[(Int, Int)].
val m1: Map[Int, Int] = (l1 zip l2)(breakOut)
val m2 = (l1, l2).zipped.toMap
没有像 lazy Map
这样的东西,因为地图中的所有元素都需要可用才能在内部构造地图,从而允许在执行时有效地检索值键查找。
因此,严格评估的 (l1 zip l2)
和惰性评估的 (l1, l2).zipped
之间的区别在转换为 Map
的行为中消失了。
那么哪个更高效?在这个特定的例子中,我希望这两种方法的表现非常相似。
在计算 m1
时,zip
操作会遍历 l1
和 l2
一次检查一对头部元素。 breakOut
构建器(另请参见下面评论中的链接)和声明的结果类型 Map[Int, Int]
,导致 zip
操作构建 Map
作为其结果(没有 breakOut
,zip
将导致在List[(Int, Int)]
)。
总结这种方法,生成的地图是通过l1
和l2
的一次同时传递创建的。
(breakOut
的使用确实有所不同。如果我们将映射生成为(l1 zip l2).toMap
,那么我们通过l1
和l2
执行一次迭代以创建List[(Int, Int)]
,然后在该列表上进行迭代创建结果Map
;这显然效率较低。
在新的 Scala 13 集合 API 中,breakOut
已被删除。但是从类型的角度来看,有一些新的替代方案效果更好。有关详细信息,请参阅this document。)
现在让我们考虑m2
。在这种情况下,如前所述,(l1, l2).zipped
会产生一个 惰性列表 元组。然而,到目前为止,还没有对任一输入列表执行迭代。当toMap
操作执行时,惰性列表中的每个元组在第一次引用时都会被评估,并添加到正在构建的映射中。
在总结此方法时,再次通过 l1
和 l2
的一次同时传递创建生成的地图。
因此,在这个特定的用例中,这两种方法之间几乎没有区别。可能仍然存在影响结果的次要实现细节,因此如果您在l1
和l2
中有大量数据,您可能仍希望对它们进行基准测试以找到最佳解决方案。但是,我倾向于简单地选择zip
操作(使用breakOut
)并保留它。
【讨论】:
我的用例是将元组转换为地图。 val m:Map[Int, Int] = (l1 zip l2)(breakOut) 和 (l1, l2).zipped.toMap 有区别吗? @EugeneMi 有趣的问题!没有像 lazy map 这样的东西,因为通过键查找值的能力需要了解整个集合。因此,当您将惰性集合转换为地图时,必须对其进行严格评估。因此,尽管Tuple2Zipped
实例是惰性的,但必须读取每个元组才能将其转换为映射。因此,在这种情况下,两种方法都会产生Map(1 -> 5, 2 -> 6, 3 -> 7)
。唯一的问题是,哪个最快?我怀疑会有很大的不同,但如果这对你很重要的话,对这两者进行基准测试并没有什么坏处。
@EugeneMi 不过,我不确定您所说的(l1 zip l2)(breakOut)
是什么意思。我假设这相当于(l1 zip l2).toMap
。
docs.scala-lang.org/tutorials/FAQ/breakout.html。它跳过创建元组的中间列表
@EugeneMi Sweet。我使用 Scala 已有 10 多年了,我不记得以前见过breakOut
!感谢您的链接! (奇怪的是,如果您在 ScalaDoc 的搜索栏中搜索它,它不会显示。)无论如何,我已经更新了我的答案以解决您第一条评论中的问题。
以上是关于Scala Tuple2Zipped vs IterableLike zip的主要内容,如果未能解决你的问题,请参考以下文章