Почему фильтр перед foldLeft медленнее в Scala?

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

Ответы

Ответ 1

Проблема, как говорили другие, заключается в том, что 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"
}

Ответ 2

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

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

вместо

(1 until 1000).filter...

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

Ответ 3

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

Ответ 4

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

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

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

Ответ 5

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