J'ai écrit du code Scala pour effectuer une opération élément par élément sur une collection. Ici, j'ai défini deux méthodes qui effectuent la même tâche. Une méthode utilise zip
et l'autre utilise 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)
Pour comparer ces deux méthodes en termes de vitesse, j'ai écrit le code suivant:
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")
}
J'appelle la fun
méthode et passe ES
et ES1
comme ci-dessous:
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)
Les résultats montrent que la méthode nommée ES1
qui utilise zipped
est plus rapide que la méthode ES
qui utilise zip
. Sur la base de ces observations, j'ai deux questions.
Pourquoi est zipped
plus rapide que zip
?
Existe-t-il un moyen encore plus rapide d'effectuer des opérations élément par élément sur une collection dans Scala?
scala
performance
scala-collections
jmh
elementwise-operations
user12140540
la source
la source
Réponses:
Pour répondre à votre deuxième question:
La triste vérité est que malgré sa concision, sa productivité améliorée et sa résilience aux bogues, les langages fonctionnels ne sont pas nécessairement les plus performants - en utilisant des fonctions d'ordre supérieur pour définir une projection à exécuter contre des collections non libres, et votre boucle serrée le met en évidence. Comme d'autres l'ont souligné, l'allocation de stockage supplémentaire pour les résultats intermédiaires et finaux aura également des frais généraux.
Si les performances sont critiques, bien que nullement universelles, dans des cas comme le vôtre, vous pouvez dérouler les opérations de Scala en équivalents impératifs afin de reprendre un contrôle plus direct sur l'utilisation de la mémoire et d'éliminer les appels de fonction.
Dans votre exemple spécifique, les
zipped
sommes peuvent être effectuées impérativement en pré-allouant un tableau fixe et modifiable de taille correcte (car zip s'arrête lorsque l'une des collections manque d'éléments), puis en ajoutant ensemble des éléments à l'index approprié (depuis l'accès à éléments de tableau par index ordinal est une opération très rapide).Ajout d'une troisième fonction
ES3
à votre suite de tests:Sur mon i7, je reçois les temps de réponse suivants:
Encore plus haineux serait de faire une mutation directe sur place du plus court des deux tableaux, ce qui corromprait évidemment le contenu de l'un des tableaux, et ne serait fait que si le tableau d'origine à nouveau n'était pas nécessaire:
Mais évidemment, la mutation directe des éléments du tableau n'est pas dans l'esprit de Scala.
la source
Array.tabulate(minSize)(i => arr(i) + arr1(i))
pour créer votre réseauArray.tabulate
devrait être beaucoup plus rapide que ce soitzip
ouzipped
ici (et est dans mes repères).for
est destiné à un appel de fonction d'ordre supérieur (foreach
). La lambda ne sera instanciée qu'une seule fois dans les deux cas.Aucune des autres réponses ne mentionne la principale raison de la différence de vitesse, à savoir que la
zipped
version évite 10 000 attributions de tuple. En tant que deux autres réponses ne note, lazip
Version implique un tableau intermédiaire, alors que lazipped
version ne pas, mais l' attribution d' un tableau pour 10.000 éléments ne sont pas ce qui fait la lazip
version tellement pire , ce sont les 10.000 tuples vécu court que sont mis dans ce tableau. Celles-ci sont représentées par des objets sur la JVM, donc vous faites un tas d'allocations d'objets pour des choses que vous allez immédiatement jeter.Le reste de cette réponse va juste dans un peu plus de détails sur la façon dont vous pouvez le confirmer.
Meilleure analyse comparative
Vous voulez vraiment utiliser un framework comme jmh pour effectuer n'importe quel type d'analyse comparative de manière responsable sur la JVM, et même dans ce cas, la partie responsable est difficile, bien que la configuration de jmh lui-même ne soit pas trop mauvaise. Si vous en avez un
project/plugins.sbt
comme ça:Et un
build.sbt
comme ça (j'utilise 2.11.8 puisque vous mentionnez que c'est ce que vous utilisez):Ensuite, vous pouvez écrire votre référence comme ceci:
Et lancez-le avec
sbt "jmh:run -i 10 -wi 10 -f 2 -t 1 zipped_bench.ZippedBench"
:Ce qui montre que la
zipped
version obtient environ 80% de débit en plus, ce qui est probablement plus ou moins le même que vos mesures.Mesurer les allocations
Vous pouvez également demander à jmh de mesurer les allocations avec
-prof gc
:… Où
gc.alloc.rate.norm
est probablement la partie la plus intéressante, montrant que lazip
version alloue plus de trois fois pluszipped
.Implémentations impératives
Si je savais que cette méthode allait être appelée dans des contextes extrêmement sensibles aux performances, je l'implémenterais probablement comme ceci:
Notez que contrairement à la version optimisée dans l'une des autres réponses, celle-ci utilise à la
while
place d'unfor
car lafor
va toujours se dissiper dans les opérations de collections Scala. Nous pouvons comparer cette implémentation (withWhile
), l' implémentation optimisée (mais pas en place) de l'autre réponse (withFor
), et les deux implémentations originales:C'est une énorme différence entre les versions impérative et fonctionnelle, et toutes ces signatures de méthode sont exactement identiques et les implémentations ont la même sémantique. Ce n'est pas comme si les implémentations impératives utilisaient l'état global, etc. Bien que les versions
zip
etzipped
soient plus lisibles, personnellement, je ne pense pas qu'il y ait un sens dans lequel les versions impératives sont contre "l'esprit de Scala", et je n'hésiterais pas de les utiliser moi-même.Avec tabuler
Mise à jour: j'ai ajouté une
tabulate
implémentation à la référence basée sur un commentaire dans une autre réponse:C'est beaucoup plus rapide que les
zip
versions, bien que beaucoup plus lent que les impératifs:C'est ce à quoi je m'attendrais, car il n'y a rien de intrinsèquement coûteux à appeler une fonction, et parce que l'accès aux éléments de tableau par index est très bon marché.
la source
Considérer
lazyZip
au lieu de
zip
Scala 2.13 ajouté
lazyZip
en faveur de.zipped
zipped
(et donclazyZip
) est plus rapide quezip
parce que , comme expliqué par Tim et Mike Allen ,zip
suivi demap
entraînera deux transformations distinctes en raison de la rigueur, tandis quezipped
suivimap
entraînera une seule transformation exécutée en une seule fois en raison de la paresse.zipped
donneTuple2Zipped
et analyseTuple2Zipped.map
,nous voyons les deux collections
coll1
etcoll2
sommes itérés sur et à chaque itération la fonctionf
passéemap
est appliquée en cours de routesans avoir à allouer et transformer des structures intermédiaires.
En appliquant la méthode d'analyse comparative de Travis, voici une comparaison entre les nouveaux
lazyZip
et les obsolèteszipped
oùdonne
lazyZip
semble fonctionner un peu mieux quezipped
surArraySeq
. Fait intéressant, notez les performances considérablement dégradées lors de l'utilisationlazyZip
surArray
.la source
Vous devez toujours être prudent avec la mesure des performances en raison de la compilation JIT, mais une raison probable est qu'il
zipped
est paresseux et extrait des éléments desArray
vaules d' origine pendant l'map
appel, alors qu'ilzip
crée un nouvelArray
objet et appelle ensuitemap
le nouvel objet.la source