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.

36
demandé sur Miguel Gamboa 2015-07-03 19:09:14

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.

  1. 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() sans hasNext() , ou hasNext() plusieurs fois sans next() , 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émentations Iterator doivent faire plus de travail pour se défendre contre les modifications concurrentes que ne le font les implémentations Spliterator .)

  2. 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.

  3. plus loin, traversal via Spliterator ont tendance à avoir beaucoup moins de tas écrit qu'avec Iterator . Avec Iterator , chaque élément provoque un ou plusieurs tas écrit (à moins que le Iterator 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 industrielle forEachRemaining 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.)

44
répondu Brian Goetz 2017-05-23 12:24:17

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:

  • Collection.forEach

    final E[] elementData = (E[]) this.elementData;
    final int size = this.size;
    for (int i=0; modCount == expectedModCount && i < size; i++) {
        action.accept(elementData[i]);
    }
    
  • Iterator.forEachRemaining

    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 appeler Spliterator.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?"

12
répondu Holger 2017-05-23 12:01:29