【问题标题】:Why is filter in front of foldLeft slow in Scala?为什么在Scala中foldLeft前面的过滤器很慢?
【发布时间】:2011-06-15 07:23:20
【问题描述】:

我写了第一个Project Euler问题的答案:

将所有小于一千且是 3 或 5 的倍数的自然数相加。

我想到的第一件事是:

(1 until 1000).filter(i => (i % 3 == 0 || i % 5 == 0)).foldLeft(0)(_ + _)

但它很慢(需要 125 毫秒),所以我重写了它,只是考虑“另一种方式”与“更快的方式”

(1 until 1000).foldLeft(0){
    (total, x) =>
        x match {
            case i if (i % 3 == 0 || i % 5 ==0) => i + total // Add
            case _ => total //skip
        }
}

这要快得多(仅 2 毫秒)。为什么?我猜第二个版本只使用 Range 生成器,并没有以任何方式表现出完全实现的集合,一次完成所有操作,速度更快,内存更少。我说的对吗?

这里是 IdeOne 上的代码:http://ideone.com/GbKlP

【问题讨论】:

  • 您如何衡量代码“快几个数量级”?在我的 非常 旧、非常 慢的笔记本电脑上,在 Scala interpreter 中,您声称“慢几个数量级”的版本需要不到 300 µs(即 micro 秒),那么 fast 版本需要多长时间?时间会倒退吗?大多数高性能 JVM 需要大约 5 来预热它们的缓存和东西,甚至在它们达到全速之前。 (HotSpot JVM 中的 C2 编译器,即优化编译器,在运行 20000 次之前甚至不会编译方法。)
  • 第二个版本似乎快了大约 3 倍(根据我在 REPL 中使用 (1 to 10000000) 的完全不科学的测量)。我不会称其为“数量级”,但仍然如此。
  • @Jörg:您可以在他的 ideone 链接上看到运行时间,但我会将这些信息编辑到问题中,这样即使 ideone 链接消失也不会丢失。
  • 你可以从这个伟大的文档中了解这个以及许多其他值得注意的事情:scala-lang.org/docu/files/collections-api/collections.html 每个人都应该在编码之前阅读这个。我知道我应该有。
  • 在 IDEOne 上看到代码我注意到了一些严重的问题。每个代码都有一个迭代,“较慢”版本首先出现。在实践中,较慢版本的内部循环是“训练”JVM 用于稍后将由“较快”版本调用的常用功能。看这里,我刚刚倒转了两段代码,哪一个更快:ideone.com/7RrlQ

标签: performance scala scala-collections


【解决方案1】:

正如其他人所说,问题在于filter 创建了一个新集合。替代的withFilter 没有,但它没有foldLeft。此外,使用.view.iterator.toStream 都可以避免以各种方式创建新集合,但在这里它们都比您使用的第一种方法慢,一开始我觉得有点奇怪。

但是,那么……看,1 until 1000 是一个Range,它的大小实际上非常小,因为它不存储每个元素。另外,Rangeforeach 进行了极大的优化,甚至是specialized,这不是任何其他集合的情况。由于foldLeft 是作为foreach 实现的,因此只要您使用Range,您就可以享受其优化的方法。

(_: Range).foreach:

@inline final override def foreach[@specialized(Unit) U](f: Int => U) {
    if (length > 0) {
        val last = this.last
        var i = start
        while (i != last) {
            f(i)
            i += step
        }
        f(i)
    }
}

(_: Range).view.foreach

def foreach[U](f: A => U): Unit = 
    iterator.foreach(f)

(_: Range).view.iterator

override def iterator: Iterator[A] = new Elements(0, length)

protected class Elements(start: Int, end: Int) extends BufferedIterator[A] with Serializable {
  private var i = start

  def hasNext: Boolean = i < end

  def next: A = 
    if (i < end) {
      val x = self(i)
      i += 1
      x
    } else Iterator.empty.next

  def head = 
    if (i < end) self(i) else Iterator.empty.next

  /** $super
   *  '''Note:''' `drop` is overridden to enable fast searching in the middle of indexed sequences.
   */
  override def drop(n: Int): Iterator[A] =
    if (n > 0) new Elements(i + n, end) else this

  /** $super
   *  '''Note:''' `take` is overridden to be symmetric to `drop`.
   */
  override def take(n: Int): Iterator[A] =
    if (n <= 0) Iterator.empty.buffered
    else if (i + n < end) new Elements(i, i + n) 
    else this
}

(_: Range).view.iterator.foreach

def foreach[U](f: A =>  U) { while (hasNext) f(next()) }

当然,这还不包括viewfoldLeft 之间的filter

override def filter(p: A => Boolean): This = newFiltered(p).asInstanceOf[This]

protected def newFiltered(p: A => Boolean): Transformed[A] = new Filtered { val pred = p }

trait Filtered extends Transformed[A] {
  protected[this] val pred: A => Boolean 
  override def foreach[U](f: A => U) {
    for (x <- self)
      if (pred(x)) f(x)
  }
  override def stringPrefix = self.stringPrefix+"F"
}

【讨论】:

  • 为了它的价值......这让我投票选出迄今为止的最佳答案。
  • 同意。我把它给了你的第一个凯文,我很抱歉改变它,但正是这个答案帮助我理解了发生了什么。
  • @arcticpenguin - 完全正确,我不会有任何其他方式!
【解决方案2】:

先尝试让集合变得懒惰,所以

(1 until 1000).view.filter...

而不是

(1 until 1000).filter...

这应该避免构建中间集合的成本。使用sum 而不是foldLeft(0)(_ + _) 也可能获得更好的性能,某些集合类型总是有可能有一种更有效的方法来求和数字。如果没有,它仍然更干净,更具声明性......

【讨论】:

  • 打败我。与sum 很好的接触。
  • 一旦我热身JVM,差异小于原帖。添加视图将其进一步降低,使其基本相同。
  • 我不了解你,但我在这里的主干上进行的测试实际上比没有view 时要慢。
  • @arcticpenguin 很好奇,我只是在这里没看到。事实上,这里有view 的版本比没有的慢。
  • 诚然,我没有对这个确切的问题进行测试,但我发现view 通常会有所帮助。当然,如果 jvm 很冷,那么所有的赌注都没有......
【解决方案3】:

查看代码,看起来 filter 确实构建了一个新的 Seq,在该 Seq 上调用了 foldLeft。第二个跳过那一点。与其说是内存,不如说是有帮助,但过滤后的集合根本就没有构建。所有这些工作都没有完成。

范围使用TranversableLike.filter,如下所示:

def filter(p: A => Boolean): Repr = {
  val b = newBuilder
  for (x <- this) 
    if (p(x)) b += x
  b.result
}

我认为区别在于第 4 行的 +=。在foldLeft 中过滤会消除它。

【讨论】:

  • 有趣。 GHC Haskell 编译器可能会在这里执行某种流融合,本质上是自己将第一个版本转换为第二个版本。不幸的是,对于像 Scala 这样的不纯语言来说,这样的事情真的很难实现(特别是如果你将动态方法调度添加到混合中)。编译器可能必须证明提供给filterfoldLeft 的块都是引用透明的,并证明没有发生有趣的子类化。
  • @Jörg W Mittag:是的。有人想知道在 Range 上调用 toStream 是否会有所帮助。可能不是。但是我们在这里很适合微优化。
【解决方案4】:

filter 创建一个全新的序列,然后调用foldLeft。试试:

(1 until 1000).view.filter(i =&gt; (i % 3 == 0 || i % 5 == 0)).reduceLeft(_+_)

这将阻止上述效果,只是包裹原来的东西。将foldLeftreduceLeft 交换只是装饰性的(在这种情况下)。

【讨论】:

  • 注意:将foldLeftreduceLeft 交换只是装饰此处,因为您碰巧先验知道该列表是非空的.一般来说,你需要弃牌以避免潜在的异常。
【解决方案5】:

现在的挑战是,你能想出一个更有效的方法吗?在这种情况下,并不是您的解决方案太慢,而是它的扩展性如何?如果不是 1000,而是 1000000000,该怎么办?有一种解决方案可以像计算前一种情况一样快速计算后一种情况。

【讨论】:

  • def arithProg(a:Int, d:Int, n:Int): Long = n * (2 * a + (n - 1) * d.toLong) / 2; def find(n: Int):Long = arithProg (3, 3, n/3) + arithProg (5, 5, n/5) - arithProg (15, 15, n/15); println(find(1000000000 - 1)) 我赢了什么?
猜你喜欢
  • 1970-01-01
  • 2019-02-10
  • 1970-01-01
  • 2015-04-28
  • 1970-01-01
  • 2016-10-22
  • 2021-06-17
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多