He escrito un código Scala para realizar una operación basada en elementos en una colección. Aquí definí dos métodos que realizan la misma tarea. Un método usa zipy el otro usa zipped.
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)
Para comparar estos dos métodos en términos de velocidad, escribí el siguiente código:
def fun (arr : Array[Double] , arr1 : Array[Double] , f :(Array[Double],Array[Double]) => Array[Double] , itr : Int) ={
  val t0 = System.nanoTime()
  for (i <- 1 to itr) {
       f(arr,arr1)
       }
  val t1 = System.nanoTime()
  println("Total Time Consumed:" + ((t1 - t0).toDouble / 1000000000).toDouble + "Seconds")
}
Llamo al funmétodo y paso ESy ES1como a continuación:
fun(Array.fill(10000)(math.random), Array.fill(10000)(math.random), ES , 100000)
fun(Array.fill(10000)(math.random), Array.fill(10000)(math.random), ES1, 100000)Los resultados muestran que el método nombrado ES1que usa zippedes más rápido que el método ESque usa zip. En base a estas observaciones, tengo dos preguntas.
¿Por qué es zippedmás rápido que zip?
¿Hay alguna forma aún más rápida de realizar operaciones basadas en elementos en una colección en Scala?
                    
                        scala
                                performance
                                scala-collections
                                jmh
                                elementwise-operations
                                
                    
                    
                        usuario12140540
fuente
                
                fuente

Respuestas:
Para responder a su segunda pregunta:
La triste verdad es que a pesar de su concisión, productividad mejorada y resistencia a los errores, los lenguajes funcionales no son necesariamente los más efectivos: el uso de funciones de orden superior para definir una proyección que se ejecutará en colecciones no gratuitas, y su ciclo cerrado lo resalta. Como otros han señalado, la asignación de almacenamiento adicional para resultados intermedios y finales también tendrá gastos generales.
Si el rendimiento es crítico, aunque de ninguna manera es universal, en casos como el suyo, puede desenrollar las operaciones de Scala en equivalentes imperativos para recuperar un control más directo sobre el uso de la memoria y eliminar las llamadas a funciones.
En su ejemplo específico, las
zippedsumas se pueden realizar imperativamente asignando previamente una matriz fija y mutable del tamaño correcto (ya que zip se detiene cuando una de las colecciones se queda sin elementos) y luego agrega elementos juntos en el índice apropiado (desde acceder elementos de matriz por índice ordinal es una operación muy rápida).Agregar una tercera función,
ES3a su conjunto de pruebas:En mi i7 obtengo los siguientes tiempos de respuesta:
Aún más audaz sería hacer una mutación directa en el lugar de las dos matrices más cortas, lo que obviamente corrompería el contenido de una de las matrices, y solo se haría si la matriz original nuevamente no fuera necesaria:
Pero obviamente, la mutación directa de los elementos de la matriz no está en el espíritu de Scala.
fuente
Array.tabulate(minSize)(i => arr(i) + arr1(i))a unaArray.tabulatedebería ser mucho más rápido que cualquierazipozippedaquí (y está en mis puntos de referencia).forse desuga a una llamada de función de orden superior (foreach). La lambda solo se instanciará una vez en ambos casos.Ninguna de las otras respuestas menciona la razón principal de la diferencia de velocidad, que es que la
zippedversión evita las asignaciones de 10.000 tuplas. Como un par de las otras respuestas hacer nota, lazipversión implica una serie intermedia, mientras que lazippedversión no, pero la asignación de un conjunto de 10.000 elementos no es lo que hace que lazipversió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.sbtcomo esto:Y
build.sbtasí (estoy usando 2.11.8 ya que mencionas que es lo que estás usando):Entonces puede escribir su punto de referencia de esta manera:
Y ejecútalo con
sbt "jmh:run -i 10 -wi 10 -f 2 -t 1 zipped_bench.ZippedBench":Lo que muestra que la
zippedversió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:... donde
gc.alloc.rate.normes probablemente la parte más interesante, que muestra que lazipversión se está asignando más de tres veceszipped.Implementaciones imperativas
Si supiera que este método se llamaría en contextos extremadamente sensibles al rendimiento, probablemente lo implementaría así:
Tenga en cuenta que, a diferencia de la versión optimizada en una de las otras respuestas, esta utiliza en
whilelugar de unaforyaforque 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: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
zipyzippedson 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
tabulateimplementación al punto de referencia basado en un comentario en otra respuesta:Es mucho más rápido que las
zipversiones, aunque sigue siendo mucho más lento que los imperativos: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.
fuente
Considerar
lazyZipen vez de
zipScala 2.13 añadido
lazyZipa favor de.zippedzipped(y, por lo tantolazyZip) es más rápido quezipporque, como lo explicaron Tim y Mike Allen ,zipseguidomapresultará en dos transformaciones separadas debido a la rigidez, mientras quezippedseguidomapdará como resultado una sola transformación ejecutada de una vez debido a la pereza.zippeddaTuple2Zipped, y analizandoTuple2Zipped.map,vemos las dos colecciones
coll1ycoll2se repiten una y otra vez en cada iteración, la función que sefpasamapse aplica a lo largo del caminosin tener que asignar y transformar estructuras intermedias.
Aplicando el método de evaluación comparativa de Travis, aquí hay una comparación entre nuevos
lazyZipy obsoletoszippeddondeda
lazyZipparece funcionar un poco mejor quezippedenArraySeq. Curiosamente, observe el rendimiento degradado significativamente cuando se utilizalazyZipenArray.fuente
Siempre debe tener cuidado con la medición del rendimiento debido a la compilación JIT, pero una razón probable es que
zippedes perezosa y extrae elementos de lasArraycápsulas originales durante lamapllamada, mientras quezipcrea un nuevoArrayobjeto y luego llamamapal nuevo objeto.fuente