Question sur la performance de Scala
Dans le article écrit par Daniel Korzekwa, il a dit que l'exécution du code suivant:
list.map(e => e*2).filter(e => e>10)
est bien pire que la solution itérative écrite en utilisant Java.
quelqu'un Peut-il expliquer pourquoi? Et quelle est la meilleure solution pour un tel code dans Scala (j'espère que ce N'est pas une version Java iterative qui est Scala-fied)?
7 réponses
La raison le code particulier est lent parce qu'il fonctionne sur les primitives mais il utilise des opérations génériques, donc les primitives doivent être boxées. (Ce qui pourrait être amélioré si
en outre, algorithmiquement, ces opérations sont un peu coûteuses, parce que vous faites une liste entière, et puis faire une liste entière nouvelle jetant quelques composants de l'intermédiaire liste de loin. Si tu le faisais en un clin d'œil, tu serais mieux. Vous pourriez faire quelque chose comme:
list collect (case e if (e*2>10) => e*2)
mais que faire si le calcul e*2
est vraiment trop cher? Alors vous pourriez
(List[Int]() /: list)((ls,e) => { val x = e*2; if (x>10) x :: ls else ls }
sauf que ceci apparaîtrait à l'envers. (Vous pouvez l'inverser si besoin est, mais cela nécessite la création d'une nouvelle liste, qui n'est pas idéale sur le plan algorithmique.)
bien sûr, vous avez le même genre de problèmes algorithmiques en Java si vous utilisez un seul liés list--votre nouvelle liste finira en arrière, ou vous devez la créer deux fois, d'abord en arrière et puis en avant, ou vous devez la construire avec la récursion (non-queue) (ce qui est facile en Scala, mais déconseillé pour ce genre de chose dans l'une ou l'autre langue puisque vous épuiserez la pile), ou vous devez créer une liste mutable et ensuite prétendre que ce n'est pas mutable. (Ce que, soit dit en passant, vous pouvez faire à Scala aussi -- voir mutable.LinkedList
.)
en gros, il traverse une liste deux fois. Une fois pour multiplier chaque élément avec deux. Et puis une autre fois pour filtrer les résultats.
pensez-y comme un tout en boucle produisant une Lienedlist avec les résultats intermédiaires. Et puis une autre boucle appliquant le filtre pour produire les résultats finaux.
Cela devrait être plus rapide:
list.view.map(e => e * 2).filter(e => e > 10).force
LA solution réside principalement dans JVM. Bien que Scala a un contournement dans la figure de @specialization
, qui augmente la taille de classe spécialisée énormément, et ne résout qu'une partie du problème, l'autre moitié étant la création d'objets temporaires.
la JVM fait en fait un bon travail en optimisant une grande partie de celui-ci, ou la performance serait encore plus terrible, mais Java ne nécessite pas les optimisations que Scala fait, donc JVM ne les fournit pas. Je pense que cela va changer pour certains mesure avec l'introduction de SAM
non-Real-closures en Java.
mais, au bout du compte, il s'agit d'équilibrer les besoins. Le même while
boucle que Java et Scala font tellement plus vite que L'équivalent de fonction de Scala peut être fait plus vite encore en C. pourtant, malgré ce que les microbenchmarks nous disent, les gens utilisent Java.
L'approche Scala est beaucoup plus abstraite et générique. Il est donc difficile d'optimiser chaque cas.
je pourrais imaginer que HotSpot JIT compiler pourrait appliquer la fusion de flux et de boucle au code dans le futur si elle voit que les résultats immédiats ne sont pas utilisés.
de plus, le code Java ne fait que beaucoup plus.
si vous voulez vraiment muter sur une structure de données, considérez transform
.
Il ressemble un peu à l' map
mais ne crée pas une nouvelle collecte, à l'e. g.:
val array = Array(1,2,3,4,5,6,7,8,9,10).transform(_ * 2)
// array is now WrappedArray(2, 4, 6, 8, 10, 12, 14, 16, 18, 20)
j'espère vraiment que d'autres opérations en place seront ajoutées à l'avenir...
pour éviter de traverser la liste deux fois, je pense que le for
la syntaxe est une option intéressante ici:
val list2 = for(v <- list1; e = v * 2; if e > 10) yield e
Rex Kerr énonce correctement le problème majeur: fonctionnant sur des listes immuables, le morceau de code indiqué crée des listes intermédiaires en mémoire. Notez que ce n'est pas nécessairement plus lent que le code Java équivalent; vous n'utilisez jamais les infrastructures de données immuables en Java.
Wilfried Springer a une bonne solution Scala idomatic. En utilisant view
, n (manipulés) exemplaires de l'ensemble de la liste sont créés.
notez que l'utilisation de view n'est pas toujours idéale. Par exemple, si votre premier appel est filter
qui devrait jeter la plupart de la liste, est peut-être la peine de créer la version la plus courte de manière explicite et utiliser view
seulement après cela afin d'améliorer la localisation de la mémoire pour les itérations ultérieures.
liste.filtre (e = > e*2>10).carte (e = > e*2)
cette tentative réduit d'abord la liste. Donc la deuxième traversée est sur moins d'éléments.