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】:

如果不明显,v1v2 的值和类型不同: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 操作会遍历 l1l2 一次检查一对头部元素。 breakOut 构建器(另请参见下面评论中的链接)和声明的结果类型 Map[Int, Int],导致 zip 操作构建 Map 作为其结果(没有 breakOutzip 将导致在List[(Int, Int)])。

总结这种方法,生成的地图是通过l1l2 的一次同时传递创建的。

(breakOut 的使用确实有所不同。如果我们将映射生成为(l1 zip l2).toMap,那么我们通过l1l2 执行一次迭代以创建List[(Int, Int)],然后在该列表上进行迭代创建结果Map;这显然效率较低。

在新的 Scala 13 集合 API 中,breakOut 已被删除。但是从类型的角度来看,有一些新的替代方案效果更好。有关详细信息,请参阅this document。)

现在让我们考虑m2。在这种情况下,如前所述,(l1, l2).zipped 会产生一个 惰性列表 元组。然而,到目前为止,还没有对任一输入列表执行迭代。当toMap 操作执行时,惰性列表中的每个元组在第一次引用时都会被评估,并添加到正在构建的映射中。

在总结此方法时,再次通过 l1l2 的一次同时传递创建生成的地图。

因此,在这个特定的用例中,这两种方法之间几乎没有区别。可能仍然存在影响结果的次要实现细节,因此如果您在l1l2 中有大量数据,您可能仍希望对它们进行基准测试以找到最佳解决方案。但是,我倾向于简单地选择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的主要内容,如果未能解决你的问题,请参考以下文章

Scala集合

Scala 列表连接,::: vs ++

vs code 安装Scala

Scala - 布尔值 - & vs &&, |与 ||

原创经验分享(17)编程实践对比Java vs Scala

Chisel3 - Chisel vs. Scala