Iterator versus Stream de Java 8
pour tirer profit de la large gamme de méthodes de requête incluses dans java.util.stream
de Jdk 8 , je suis tenté de concevoir des modèles de domaine où getters de relation avec *
multiplicité (avec zéro ou plusieurs instances ) retourner un Stream<T>
, au lieu d'un Iterable<T>
ou Iterator<T>
.
mon doute est s'il y a des frais généraux supplémentaires encourus par le Stream<T>
par rapport au Iterator<T>
?
donc, y a-t-il inconvénient de compromettre mon modèle de domaine avec un Stream<T>
?
ou plutôt, devrais-je toujours retourner un Iterator<T>
ou Iterable<T>
, et laisser à l'utilisateur la décision de choisir d'utiliser ou non un flux, en convertissant cet itérateur avec le StreamUtils
?
Note que le retour d'un Collection
n'est pas une option valide parce que dans ce cas la plupart des relations sont paresseuses et de taille inconnue.
2 réponses
il y a beaucoup de conseils sur la performance ici, mais malheureusement, il s'agit en grande partie de conjectures, et peu d'entre elles pointent vers les véritables considérations sur la performance.
@Holger il , en soulignant que nous devons résister à la apparemment irrésistible tendance à laisser les performances de la queue de l'arbre qui cache la conception d'API chien.
bien qu'il existe un zillion de considérations qui peuvent rendre un cours d'eau plus lent que, le même que, ou plus rapide que une autre forme de traversée dans tous les cas, il y a des facteurs qui indiquent que les flux ont un avantage de performance là où ils comptent -- sur les ensembles de données volumineux.
il y a des frais généraux de démarrage fixes supplémentaires de créant a Stream
comparé à la création d'un Iterator
-- quelques objets de plus avant de commencer à calculer. Si votre ensemble de données est grand, cela n'a pas d'importance; c'est un petit coût de démarrage amorti sur beaucoup de calcul. (Et si votre ensemble de données est petit, cela n'a probablement pas non plus d'importance -- parce que si votre programme fonctionne sur de petits ensembles de données, la performance n'est généralement pas votre préoccupation n ° 1 non plus.) Où ce fait est en parallèle; tout le temps passé à configurer le pipeline va dans la fraction sérielle de la loi D'Amdahl; si vous regardez la mise en œuvre, nous travaillons dur pour garder le compte d'objet vers le bas pendant la configuration de flux, mais je serais heureux de trouver des moyens de le réduire car cela a un effet direct sur la taille de l'ensemble de données breakeven où parallel commence à gagner sur sequential.
mais, plus important que le coût de démarrage fixe est le coût d'accès par élément. Ici, les cours d'eau gagnent en fait -- et gagnent souvent Gros -- ce que certains peuvent trouver surprenant. (Dans nos tests de performance, nous voyons régulièrement des pipelines de flux qui peuvent surpasser leurs homologues pour-boucle sur Collection
.) Et, il y a une explication simple à cela: Spliterator
a fondamentalement des coûts d'accès par élément inférieurs à Iterator
, même séquentiellement. Il y a plusieurs raisons à cela.
-
le protocole itérateur est fondamentalement moins efficace. Il faut appeler deux méthodes pour obtenir chaque élément. En outre, parce que les itérateurs doivent être robustes à des choses comme appeler
next()
sanshasNext()
, ouhasNext()
plusieurs fois sansnext()
, ces deux méthodes ont généralement à faire un codage défensif (et généralement plus de statessness et de branching), ce qui ajoute à l'inefficacité. D'un autre côté, même la manière lente de traverser un spliterator (tryAdvance
) n'a pas ce fardeau. (C'est encore pire pour les structures de données concurrentes, parce que la dualiténext
/hasNext
est fondamentalement racée, et les implémentationsIterator
doivent faire plus de travail pour se défendre contre les modifications concurrentes que ne le font les implémentationsSpliterator
.) -
Spliterator
offre en outre une itération "fast-path" --forEachRemaining
-- qui peut être utilisé la plupart du temps (réduction, forEach), réduisant encore plus le plafond du code d'itération qui médiate l'accès à la structure de données internes. Cela tend aussi très bien à l'inline, ce qui à son tour augmente l'efficacité d'autres optimisations telles que le mouvement du code, l'élimination des limites de contrôle, etc. -
plus loin, traversal via
Spliterator
ont tendance à avoir beaucoup moins de tas écrit qu'avecIterator
. AvecIterator
, chaque élément provoque un ou plusieurs tas écrit (à moins que leIterator
peut être scalarisé par l'analyse de fuite et ses champs hissés dans des registres.) Entre autres, cela entraîne une activité de marque de carte du GC, ce qui entraîne une contestation de la ligne de cache pour les marques de carte. D'autre part,Spliterators
ont tendance à avoir moins d'état, et la puissance industrielleforEachRemaining
mises en œuvre ont tendance à différer l'écriture de quelque chose à la tas jusqu'à la fin de la traversée, au lieu de stocker son état d'itération dans des locaux qui se déplacent naturellement vers des registres, ce qui entraîne une réduction de l'activité du bus mémoire.
résumé: ne vous inquiétez pas, soyez heureux. Spliterator
est un meilleur Iterator
, même sans parallélisme. (Ils sont aussi généralement plus facile à écrire et plus difficile de se tromper.)
comparons l'opération courante d'itération sur tous les éléments, en supposant que la source est un ArrayList
. Ensuite, il y a trois moyens standard pour y parvenir:
-
final E[] elementData = (E[]) this.elementData; final int size = this.size; for (int i=0; modCount == expectedModCount && i < size; i++) { action.accept(elementData[i]); }
-
final Object[] elementData = ArrayList.this.elementData; if (i >= elementData.length) { throw new ConcurrentModificationException(); } while (i != size && modCount == expectedModCount) { consumer.accept((E) elementData[i++]); }
-
Stream.forEach
qui finira par appelerSpliterator.forEachRemaining
if ((i = index) >= 0 && (index = hi) <= a.length) { for (; i < hi; ++i) { @SuppressWarnings("unchecked") E e = (E) a[i]; action.accept(e); } if (lst.modCount == mc) return; }
comme vous pouvez le voir, la boucle interne du code de mise en œuvre, où ces opérations se terminent, est essentiellement la même, itérant sur les indices et lisant directement le tableau et passant l'élément au Consumer
.
des choses similaires s'appliquent à toutes les normes collections de la JRE, chacun d'eux ont adapté les implémentations pour toutes les façons de le faire, même si vous utilisez un papier d'emballage en lecture seule. Dans ce dernier cas, l'API Stream
gagnerait même légèrement, Collection.forEach
doit être appelé sur la vue en lecture seule afin de déléguer à la collection originale forEach
. De même, l'itérateur doit être enveloppé pour protéger contre les tentatives d'invoquer la méthode remove()
. En revanche, spliterator()
peut retourner directement les Spliterator
comme il n'a pas de support de modification. Ainsi, le flot d'une vue en lecture seule est exactement le même que le flux de la collection d'origine.
bien que toutes ces différences soient à peine à remarquer lors de la mesure de la performance réelle comme, comme dit, la boucle interne , qui est la chose la plus importante de performance, est la même dans tous les cas.
la question Est de savoir quelle conclusion en tirer. Vous pouvez toujours retourner une vue d'ensemble en lecture seule de la collection originale, car l'appelant peut encore invoquer stream().forEach(…)
pour itérer directement dans le contexte de la collection originale.
puisque la performance n'est pas vraiment différente, vous devriez plutôt vous concentrer sur le design de niveau supérieur comme discuté dans "devrais-je retourner une Collection ou un flux?"