Почему фильтр перед foldLeft работает медленно в Scala? - PullRequest
21 голосов
/ 25 января 2011

я написал ответ на первый 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

Ответы [ 5 ]

24 голосов
/ 25 января 2011

Проблема, как говорили другие, заключается в том, что filter создает новую коллекцию.Альтернатива withFilter не имеет, но у нее нет foldLeft.Кроме того, использование .view, .iterator или .toStream позволит избежать создания новой коллекции различными способами, но здесь все они работают медленнее, чем первый использованный вами метод, который сначала мне показался несколько странным.

Но тогда ... Понимаете, 1 until 1000 - это Range, размер которого на самом деле очень мал, потому что он не хранит каждый элемент.Кроме того, Range foreach чрезвычайно оптимизирован и даже 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()) }

И это, конечно, даже не считается filterмежду view и foldLeft:

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"
}
12 голосов
/ 25 января 2011

Попробуйте сначала сделать коллекцию ленивой, поэтому

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

вместо

(1 until 1000).filter...

Это должно избежать затрат на создание промежуточной коллекции. Вы также можете получить лучшую производительность от использования sum вместо foldLeft(0)(_ + _), всегда возможно, что у некоторого типа коллекции будет более эффективный способ суммирования чисел. Если нет, то все еще чище и более декларативно ...

3 голосов
/ 25 января 2011

Просматривая код, похоже, что filter создает новый 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 устраняет это.

1 голос
/ 30 января 2011

Теперь проблема в том, можете ли вы придумать еще более эффективный способ?Не то, чтобы ваше решение было слишком медленным в этом случае, но насколько хорошо оно масштабируется?Что если вместо 1000 это будет 1000000000?Существует решение, которое может вычислить последний случай так же быстро, как и первый.

1 голос
/ 25 января 2011

filter создает совершенно новую последовательность, для которой затем вызывается foldLeft. Попробуйте:

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

Это предотвратит указанный эффект, просто обернув оригинальную вещь. Обмен foldLeft на reduceLeft только косметический (в данном случае).

...