【问题标题】:Scala's toList function appears to be slowScala 的 toList 函数似乎很慢
【发布时间】:2015-02-06 20:46:27
【问题描述】:

我的印象是在不可变的 Seq 上调用 seq.toList() 将创建一个新列表,该列表共享第一个列表的结构状态。我们发现这可能真的很慢,我不知道为什么。 只是共享结构状态,对吗?我不明白为什么当它知道它们永远不会改变时,它会制作所有元素的 n 次副本。

【问题讨论】:

  • 您能否指定将其归类为快速的标准是什么?
  • 所以我认为标准是它看起来比仅仅更新一个简单的对象要慢得多。我的同事进行简单测试实际上是在将它与 JWrappers 进行比较(使用隐式转换将 seq 转换为列表。我的意思是 - 当我们运行超过 1M 项时,不同之处在于 toList 函数是两倍慢(JWrappers 和隐式转换为 15 秒,toList 为 30 秒。我必须在这里遗漏一些东西。
  • 显示您的代码?我们只能在这里猜测您的基准是什么。
  • 我们正在使用 [1] 中的第三个策略,以便我们可以在 Scala 2.10 中使用 Collections.binarySearch()。注意这里的 import java.util.List => JList 以及它如何使用 asScalaList 隐式进行从 a.toList 到 JList[T] 的隐式转换。如果我们在没有 toList 的情况下这样做,它会很快,添加 toList 会很慢。有任何想法吗? [1]stackoverflow.com/questions/4226947/…
  • 调用 seq.iterator.toList 似乎是共享的结构状态 - 因此它的速度很快。还有人看到这个吗?

标签: scala


【解决方案1】:

Scala 中的List 是一种特殊的数据结构::: 的实例每个都包含一个值,然后在链的末尾是Nil

如果您toListList,则需要O(1) 时间。如果您在其他任何东西上使用toList,则必须将其转换为List,这涉及O(n) 对象分配(所有:: 实例)。

所以你必须询问你是否真的想要scala.collection.immutable.List。这就是toList 给你的。

【讨论】:

  • 我完全理解这一点,但 iterator.toList 似乎是恒定时间。也许我会在幕后做更多的研究来弄清楚为什么会这样
  • @CoreyJ.Nolet - 你只是测量错误。你会得到近乎完美的O(n),比如(1 to n).iterator.toList
  • 如果我测量错误,那么为什么 seq.toList 慢了几个数量级。另外,您如何确定这一点?到 iterator.toList 大约是 100 毫秒,而 seq.toList 大约是 10 秒。还缺少什么?如果两者都是 o(n),那么我可以想象对象创建时间不会是这里的障碍。
  • @CoreyJ.Nolet - 因为你测量错误。 (1 to n).toList 更快。我正在运行微基准测试。你在做什么需要几十秒?在那之前你应该会用完内存。
  • 看我上面的例子。 (0 to n).toList 不是我在做什么。对 (0 到 512).toList 进行 1000000 次迭代,并将其与 (0 到 512).iterator.toList() 的 1000000 次迭代进行比较,然后告诉我哪个似乎更慢......还请告诉我我是如何测量错误的。从我收集的信息来看,(我还没有在调试器中完全完成这个)是 iterator.toList 似乎正在包装与另一个 Range.toList 似乎正在创建一个新的 ListBuffer (导致它为 O(n)。后一部分我在调试器中逐步完成并验证。
【解决方案2】:

对于特定数据结构的特定操作,共享结构状态是可能的。

对于Scala中的List数据结构,我的理解是每个元素都指向下一个元素,从头到尾,所以是一个单链表。

从结构状态共享的角度来看,从内部数据结构的角度考虑对此的限制。将元素添加到列表 (X) 的头部有效地创建了一个新列表 (X'),其中新元素作为 X' 的头部,旧列表 (X) 作为尾部。对于这个特定的操作,内部状态可以完全共享。

同样的操作可以用来创建一个新的List(X'),新的元素作为X'的头,X中的任何元素作为尾,只要你接受尾将是您从 X 中选择的元素,以及它的数据结构中已有的所有其他元素。

从逻辑上考虑,每个数据结构都有一个内部结构,允许使用简单的共享内部结构执行一些操作,而其他操作则需要更具侵入性和成本更高的计算。

在我看来,这里的关键是了解内部数据结构本身对操作的约束。

例如,考虑上面对双向链表数据结构的相同操作,您会发现存在完全不同的限制。

就个人而言,我发现了解内部结构有助于理解特定操作的后果。

在对任意序列的 toList 操作的情况下,不知道任意序列的内部数据结构,因此必须假设 O(n)。 List.toList 已经是一个列表,具有明显的性能优势。

【讨论】:

  • 请参阅我从 Orderskys“scala 编程”一书中了解所有内容,但为什么迭代器能够在恒定时间内变成列表而不是序列。我认为我对 Seq.toList 的第一个假设是会有一个包装器来执行 xonversion,因此只有在调用 size 或 foreach 操作时 O(n) 才可能是最坏的情况。我也没有看到任何关于这些语义的文档,这使得它需要反复试验才能弄清楚。
猜你喜欢
  • 1970-01-01
  • 2021-12-11
  • 1970-01-01
  • 1970-01-01
  • 2011-07-03
  • 2015-02-16
  • 1970-01-01
  • 1970-01-01
  • 2016-09-08
相关资源
最近更新 更多