Для понимания и количества создания функции

Недавно у меня было интервью для позиции Scala Developer. Меня задали такой вопрос

// matrix 100x100 (content unimportant)

val matrix = Seq.tabulate(100, 100) { case (x, y) => x + y }

// A

for {

   row <- matrix

   elem <- row

} print(elem)

// B

val func = print _
for {

   row <- matrix

   elem <- row

} func(elem)

и вопрос: какая реализация, A или B, более эффективна?

Мы все знаем, что для понимания можно перевести на

// A

matrix.foreach(row => row.foreach(elem => print(elem)))

// B

matrix.foreach(row => row.foreach(func))

B можно записать как matrix.foreach(row => row.foreach(print _))

Предположительно правильный ответ - B, потому что A создаст функцию print в 100 раз больше.

Я проверил Спецификацию языка, но все еще не понимаю ответа. Может ли кто-нибудь объяснить это мне?

Ответы

Ответ 1

Короче:

Пример А быстрее в теории, на практике вам не удастся измерить какую-либо разницу.

Длинный ответ:

Как вы уже узнали

for {xs <- xxs; x <- xs} f(x)

переводится на

xxs.foreach(xs => xs.foreach(x => f(x)))

Это объясняется в §6.19. SLS:

A для цикла

for ( p <- e; p' <- e' ... ) e''

где... - (возможно, пустая) последовательность генераторов, определений или охранников, переводится на

e .foreach { case p => for ( p' <- e' ... ) e'' }

Теперь, когда вы пишете литерал функции, каждый получает функцию при каждом вызове функции (§6.23 SLS). Это означает, что

xs.foreach(x => f(x))

эквивалентно

xs.foreach(new scala.Function1 { def apply(x: T) = f(x)})

Когда вы вводите локальный тип функции

val g = f _; xxs.foreach(xs => xs.foreach(x => g(x)))

вы не вводите оптимизацию, потому что вы все еще передаете литерал функции foreach. На самом деле код медленнее, потому что внутренний foreach переведен на

xs.foreach(new scala.Function1 { def apply(x: T) = g.apply(x) })

где происходит дополнительный вызов метода apply g. Хотя, вы можете оптимизировать, когда пишете

val g = f _; xxs.foreach(xs => xs.foreach(g))

потому что внутренний foreach теперь переведен на

xs.foreach(g())

что означает, что сама функция g передается foreach.

Это означало бы, что B быстрее в теории, потому что никакая анонимная функция не должна создаваться каждый раз, когда выполняется тело для понимания. Однако упомянутая выше оптимизация (то, что функция напрямую передается на foreach) не применяется для понимания, поскольку, поскольку спецификация говорит, что перевод включает в себя создание функциональных литералов, поэтому всегда создаются ненужные объекты функций (здесь я должен сказать, что компилятор также мог бы это оптимизировать, но это не потому, что оптимизация для понимания сложна и в 2.11 не происходит). В целом это означает, что A более эффективен, но B будет более эффективным, если он написан без понимания (и для функции самой внутренней функции не создается литерал функции).

Тем не менее, все эти правила могут применяться только теоретически, поскольку на практике есть бэкэнд scalac и сам JVM, которые оба могут делать оптимизации - не говоря уже об оптимизации, которые выполняет процессор. Кроме того, ваш пример содержит сценарий, который выполняется на каждой итерации - это, вероятно, самая дорогая операция, которая перевешивает все остальное.

Ответ 2

Я согласен с sschaef и скажу, что A является более эффективным вариантом.

Глядя на сгенерированные файлы классов, мы получаем следующие анонимные функции и методы их применения:

MethodA:
  anonfun$2            -- row => row.foreach(new anonfun$2$$anonfun$1)
  anonfun$2$$anonfun$1 -- elem => print(elem)

то есть. matrix.foreach(row => row.foreach(elem => print(elem)))

MethodB:
  anonfun$3            -- x => print(x)
  anonfun$4            -- row => row.foreach(new anonfun$4$$anonfun$2)
  anonfun$4$$anonfun$2 -- elem => func(elem)

то есть. matrix.foreach(row => row.foreach(elem => func(elem))) где func - это еще одно косвенное выражение перед вызовом print. Кроме того, нужно искать func, т.е. Через вызов метода для экземпляра (this.func()) для каждой строки.

Итак, для метода B создается 1 дополнительный объект (func) и есть # of elem дополнительные вызовы функций.

Наиболее эффективным вариантом будет

matrix.foreach(row => row.foreach(func))

поскольку у этого есть наименьшее количество объектов, созданных и выполняемых точно так, как вы ожидали.

Ответ 3

Benchmark

Резюме

Метод A почти на 30% быстрее, чем метод B.

Ссылка на код: https://gist.github.com/ziggystar/490f693bc39d1396ef8d

Сведения о реализации

Я добавил метод C (два цикла while) и D (fold, sum). Я также увеличил размер матрицы и вместо этого использовал IndexedSeq. Также я заменил print чем-то менее тяжелым (суммировать все записи).

Странно, что конструкция while не самая быстрая. Но если использовать Array вместо IndexedSeq, он становится самым быстрым с большим отрывом (фактор 5, больше не бокс). Используя явно вложенные в ящики целые числа, методы A, B, C все одинаково быстры. В частности, они быстрее на 50% по сравнению с неявно коробочными версиями A, B.

Результаты

A
4.907797735
4.369745787
4.375195012000001
4.7421321800000005
4.35150636
B
5.955951859000001
5.925475619
5.939570085000001
5.955592247
5.939672226000001
C
5.991946029
5.960122757000001
5.970733164
6.025532582
6.04999499
D
9.278486201
9.265983922
9.228320372
9.255641645
9.22281905
verify results
999000000
999000000
999000000
999000000

>$ scala -version
Scala code runner version 2.11.0 -- Copyright 2002-2013, LAMP/EPFL

Выдержка кода

val matrix = IndexedSeq.tabulate(1000, 1000) { case (x, y) => x + y }

def variantA(): Int = {
  var r = 0
  for {
    row <- matrix
    elem <- row
  }{
    r += elem
  }
  r
}

def variantB(): Int = {
  var r = 0
  val f = (x:Int) => r += x
  for {
    row <- matrix
    elem <- row
  } f(elem)
  r
}

def variantC(): Int = {
  var r = 0
  var i1 = 0
  while(i1 < matrix.size){
    var i2 = 0
    val row = matrix(i1)
    while(i2 < row.size){
      r += row(i2)
      i2 += 1
    }
    i1 += 1
  }
  r
}

def variantD(): Int = matrix.foldLeft(0)(_ + _.sum)