Ninguna de las otras respuestas menciona la razón principal de la diferencia de velocidad, que es que la zipped
versión evita las asignaciones de 10.000 tuplas. Como un par de las otras respuestas hacer nota, la zip
versión implica una serie intermedia, mientras que la zipped
versión no, pero la asignación de un conjunto de 10.000 elementos no es lo que hace que la zip
versión mucho peor-se las 10.000 tuplas de corta duración que se están poniendo en esa matriz. Estos están representados por objetos en la JVM, por lo que está haciendo un montón de asignaciones de objetos para cosas que inmediatamente va a tirar.
El resto de esta respuesta solo entra en un poco más de detalles sobre cómo puede confirmar esto.
Mejor benchmarking
Realmente desea utilizar un marco como jmh para realizar cualquier tipo de evaluación comparativa de manera responsable en la JVM, e incluso entonces la parte responsable es difícil, aunque configurar jmh en sí no es tan malo. Si tienes algo project/plugins.sbt
como esto:
addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.3.7")
Y build.sbt
así (estoy usando 2.11.8 ya que mencionas que es lo que estás usando):
scalaVersion := "2.11.8"
enablePlugins(JmhPlugin)
Entonces puede escribir su punto de referencia de esta manera:
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)
}
Y ejecútalo con 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
Lo que muestra que la zipped
versión obtiene aproximadamente un 80% más de rendimiento, lo que probablemente es más o menos lo mismo que sus mediciones.
Medición de asignaciones
También puede pedirle a jmh que mida las asignaciones con -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
... donde gc.alloc.rate.norm
es probablemente la parte más interesante, que muestra que la zip
versión se está asignando más de tres veces zipped
.
Implementaciones imperativas
Si supiera que este método se llamaría en contextos extremadamente sensibles al rendimiento, probablemente lo implementaría así:
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
}
Tenga en cuenta que, a diferencia de la versión optimizada en una de las otras respuestas, esta utiliza en while
lugar de una for
ya for
que todavía se desugará en las operaciones de colecciones Scala. Podemos comparar esta implementación ( withWhile
), la implementación optimizada (pero no en el lugar) de la otra respuesta ( withFor
) y las dos implementaciones originales:
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
Esa es una gran diferencia entre las versiones imperativas y funcionales, y todas estas firmas de métodos son exactamente idénticas y las implementaciones tienen la misma semántica. No es como si las implementaciones imperativas estuvieran usando un estado global, etc. Si bien las versiones zip
y zipped
son más legibles, personalmente no creo que tenga sentido que las versiones imperativas estén en contra del "espíritu de Scala", y no dudaría para usarlos yo mismo.
Con tabular
Actualización: agregué una tabulate
implementación al punto de referencia basado en un comentario en otra respuesta:
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))
}
Es mucho más rápido que las zip
versiones, aunque sigue siendo mucho más lento que los imperativos:
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
Esto es lo que esperaría, ya que no hay nada inherentemente costoso en llamar a una función, y porque acceder a los elementos de la matriz por índice es muy barato.