Ни в одном из других ответов не упоминается основная причина разницы в скорости, заключающаяся в том, что zipped
версия избегает 10000 распределений кортежей. Как пара других ответов сделать примечание, то zip
версия включает в себя промежуточный массив, в то время как zipped
версия не делает, но выделять массив 10000 элементов не то , что делает zip
версию намного хуже, это 10.000 короткоживущих кортежей , которые помещаются в этот массив. Они представлены объектами в JVM, поэтому вы делаете кучу распределений объектов для вещей, которые вы немедленно собираетесь выбросить.
Оставшаяся часть этого ответа просто подробнее расскажет о том, как вы можете это подтвердить.
Лучший бенчмаркинг
Вы действительно хотите использовать инфраструктуру, такую как jmh, для ответственного проведения любых сравнительных тестов в JVM, и даже тогда ответственно сложно, хотя настройка самого jmh не так уж и плоха. Если у вас есть project/plugins.sbt
такие:
addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.3.7")
И build.sbt
вот так (я использую 2.11.8, так как вы упоминаете, что вы используете):
scalaVersion := "2.11.8"
enablePlugins(JmhPlugin)
Затем вы можете написать свой тест следующим образом:
package zipped_bench
import org.openjdk.jmh.annotations._
@State(Scope.Benchmark)
@BenchmarkMode(Array(Mode.Throughput))
class ZippedBench {
val arr1 = Array.fill(10000)(math.random)
val arr2 = Array.fill(10000)(math.random)
def ES(arr: Array[Double], arr1: Array[Double]): Array[Double] =
arr.zip(arr1).map(x => x._1 + x._2)
def ES1(arr: Array[Double], arr1: Array[Double]): Array[Double] =
(arr, arr1).zipped.map((x, y) => x + y)
@Benchmark def withZip: Array[Double] = ES(arr1, arr2)
@Benchmark def withZipped: Array[Double] = ES1(arr1, arr2)
}
И запустить его с sbt "jmh:run -i 10 -wi 10 -f 2 -t 1 zipped_bench.ZippedBench"
:
Benchmark Mode Cnt Score Error Units
ZippedBench.withZip thrpt 20 4902.519 ± 41.733 ops/s
ZippedBench.withZipped thrpt 20 8736.251 ± 36.730 ops/s
Что показывает, что zipped
версия получает примерно на 80% больше пропускной способности, что, вероятно, более или менее соответствует вашим измерениям.
Измерительные распределения
Вы также можете попросить JMH измерить распределение с -prof gc
:
Benchmark Mode Cnt Score Error Units
ZippedBench.withZip thrpt 5 4894.197 ± 119.519 ops/s
ZippedBench.withZip:·gc.alloc.rate thrpt 5 4801.158 ± 117.157 MB/sec
ZippedBench.withZip:·gc.alloc.rate.norm thrpt 5 1080120.009 ± 0.001 B/op
ZippedBench.withZip:·gc.churn.PS_Eden_Space thrpt 5 4808.028 ± 87.804 MB/sec
ZippedBench.withZip:·gc.churn.PS_Eden_Space.norm thrpt 5 1081677.156 ± 12639.416 B/op
ZippedBench.withZip:·gc.churn.PS_Survivor_Space thrpt 5 2.129 ± 0.794 MB/sec
ZippedBench.withZip:·gc.churn.PS_Survivor_Space.norm thrpt 5 479.009 ± 179.575 B/op
ZippedBench.withZip:·gc.count thrpt 5 714.000 counts
ZippedBench.withZip:·gc.time thrpt 5 476.000 ms
ZippedBench.withZipped thrpt 5 11248.964 ± 43.728 ops/s
ZippedBench.withZipped:·gc.alloc.rate thrpt 5 3270.856 ± 12.729 MB/sec
ZippedBench.withZipped:·gc.alloc.rate.norm thrpt 5 320152.004 ± 0.001 B/op
ZippedBench.withZipped:·gc.churn.PS_Eden_Space thrpt 5 3277.158 ± 32.327 MB/sec
ZippedBench.withZipped:·gc.churn.PS_Eden_Space.norm thrpt 5 320769.044 ± 3216.092 B/op
ZippedBench.withZipped:·gc.churn.PS_Survivor_Space thrpt 5 0.360 ± 0.166 MB/sec
ZippedBench.withZipped:·gc.churn.PS_Survivor_Space.norm thrpt 5 35.245 ± 16.365 B/op
ZippedBench.withZipped:·gc.count thrpt 5 863.000 counts
ZippedBench.withZipped:·gc.time thrpt 5 447.000 ms
…где gc.alloc.rate.norm
, пожалуй, самая интересная часть, показывающая, что zip
версия выделяется в три раза больше, чем zipped
.
Императивные реализации
Если бы я знал, что этот метод будет вызываться в чрезвычайно чувствительных к производительности контекстах, я бы, вероятно, реализовал его следующим образом:
def ES3(arr: Array[Double], arr1: Array[Double]): Array[Double] = {
val minSize = math.min(arr.length, arr1.length)
val newArr = new Array[Double](minSize)
var i = 0
while (i < minSize) {
newArr(i) = arr(i) + arr1(i)
i += 1
}
newArr
}
Обратите внимание, что в отличие от оптимизированной версии в одном из других ответов, она использует while
вместо a, for
поскольку for
все равно будет десагар в операциях с коллекциями Scala. Мы можем сравнить эту реализацию ( withWhile
), оптимизированную (но не на месте) реализацию другого ответа ( withFor
) и две исходные реализации:
Benchmark Mode Cnt Score Error Units
ZippedBench.withFor thrpt 20 118426.044 ± 2173.310 ops/s
ZippedBench.withWhile thrpt 20 119834.409 ± 527.589 ops/s
ZippedBench.withZip thrpt 20 4886.624 ± 75.567 ops/s
ZippedBench.withZipped thrpt 20 9961.668 ± 1104.937 ops/s
Это действительно огромная разница между императивной и функциональной версиями, и все эти сигнатуры методов абсолютно идентичны, а реализации имеют одинаковую семантику. Это не так, как императивные реализации используют глобальное состояние и т. Д. В то время как zip
иzipped
версия более читаемая, лично я не думаю , что есть какой -то смысл , в котором императивные версии против «духа Scala», и я бы не колеблясь , использовать их сам.
С таблицей
Обновление: я добавил tabulate
реализацию в тест на основе комментария в другом ответе:
def ES4(arr: Array[Double], arr1: Array[Double]): Array[Double] = {
val minSize = math.min(arr.length, arr1.length)
Array.tabulate(minSize)(i => arr(i) + arr1(i))
}
Это намного быстрее, чем zip
версии, хотя все еще намного медленнее, чем императивные:
Benchmark Mode Cnt Score Error Units
ZippedBench.withTabulate thrpt 20 32326.051 ± 535.677 ops/s
ZippedBench.withZip thrpt 20 4902.027 ± 47.931 ops/s
Это то, что я ожидаю, поскольку нет ничего изначально дорогого в вызове функции, и потому что доступ к элементам массива по индексу очень дешев.