Flux Java 8: plusieurs filtres vs condition complexe

235

Parfois, vous souhaitez filtrer un Streamavec plusieurs conditions:

myList.stream().filter(x -> x.size() > 10).filter(x -> x.isCool()) ...

ou vous pouvez faire de même avec une condition complexe et une seule filter :

myList.stream().filter(x -> x.size() > 10 && x -> x.isCool()) ...

Je suppose que la deuxième approche a de meilleures caractéristiques de performance, mais je ne le sais pas.

La première approche gagne en lisibilité, mais quoi de mieux pour les performances?

démon
la source
57
Écrivez le code le plus lisible dans la situation. La différence de performances est minime (et hautement situationnelle).
Brian Goetz
5
Oubliez les nano-optimisations et utilisez du code hautement lisible et maintenable. avec les flux, il faut toujours utiliser chaque opération séparément, y compris les filtres.
Diablo

Réponses:

151

Le code qui doit être exécuté pour les deux alternatives est si similaire que vous ne pouvez pas prédire un résultat de manière fiable. La structure d'objet sous-jacente peut différer, mais cela ne représente pas un défi pour l'optimiseur de hotspot. Cela dépend donc d'autres conditions environnantes qui se traduiront par une exécution plus rapide, s'il y a une différence.

La combinaison de deux instances de filtre crée plus d'objets et donc plus de code de délégation, mais cela peut changer si vous utilisez des références de méthode plutôt que des expressions lambda, par exemple, remplacez filter(x -> x.isCool())par filter(ItemType::isCool). De cette façon, vous avez éliminé la méthode de délégation synthétique créée pour votre expression lambda. Ainsi, la combinaison de deux filtres à l'aide de deux références de méthode peut créer le même code de délégation ou moins qu'un filterappel unique utilisant une expression lambda avec &&.

Mais, comme indiqué, ce type de surcharge sera éliminé par l'optimiseur HotSpot et est négligeable.

En théorie, deux filtres pourraient être plus faciles à paralléliser qu'un seul filtre, mais cela n'est pertinent que pour des tâches intenses plutôt informatiques¹.

Il n'y a donc pas de réponse simple.

En fin de compte, ne pensez pas à ces différences de performances en dessous du seuil de détection des odeurs. Utilisez ce qui est plus lisible.


¹… et nécessiterait une implémentation faisant un traitement parallèle des étapes suivantes, une route actuellement non empruntée par l'implémentation Stream standard

Holger
la source
4
le code n'a-t-il pas à itérer le flux résultant après chaque filtre?
jucardi
13
@Juan Carlos Diaz: non, les streams ne fonctionnent pas de cette façon. Lisez à propos de «l'évaluation paresseuse»; les opérations intermédiaires ne font rien, elles ne modifient que le résultat de l'opération du terminal.
Holger
34

Une condition de filtre complexe est meilleure du point de vue des performances, mais les meilleures performances montreront l'ancienne mode pour la boucle avec une norme if clause est la meilleure option. La différence sur un petit tableau de 10 éléments peut être ~ 2 fois, pour un grand tableau, la différence n'est pas si grande.
Vous pouvez jeter un œil à mon projet GitHub , où j'ai fait des tests de performances pour plusieurs options d'itération de tableau

Pour les ops / s de débit à 10 éléments de petite baie: Tableau à 10 éléments Pour les ops / s de débit moyen à 10 000 éléments: entrez la description de l'image ici débit à 1 000 000 d'éléments pour grande baie: Éléments 1M

REMARQUE: les tests s'exécutent sur

  • 8 CPU
  • 1 Go de RAM
  • Version du système d'exploitation: 16.04.1 LTS (Xenial Xerus)
  • version java: 1.8.0_121
  • jvm: -XX: + UseG1GC -server -Xmx1024m -Xms1024m

METTRE À JOUR: Java 11 a quelques progrès sur les performances, mais la dynamique reste la même

Mode de référence: débit, opérations / temps Java 8vs11

Serge
la source
22

Ce test montre que votre deuxième option peut être beaucoup plus performante. Les résultats d'abord, puis le code:

one filter with predicate of form u -> exp1 && exp2, list size 10000000, averaged over 100 runs: LongSummaryStatistics{count=100, sum=4142, min=29, average=41.420000, max=82}
two filters with predicates of form u -> exp1, list size 10000000, averaged over 100 runs: LongSummaryStatistics{count=100, sum=13315, min=117, average=133.150000, max=153}
one filter with predicate of form predOne.and(pred2), list size 10000000, averaged over 100 runs: LongSummaryStatistics{count=100, sum=10320, min=82, average=103.200000, max=127}

maintenant le code:

enum Gender {
    FEMALE,
    MALE
}

static class User {
    Gender gender;
    int age;

    public User(Gender gender, int age){
        this.gender = gender;
        this.age = age;
    }

    public Gender getGender() {
        return gender;
    }

    public void setGender(Gender gender) {
        this.gender = gender;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

static long test1(List<User> users){
    long time1 = System.currentTimeMillis();
    users.stream()
            .filter((u) -> u.getGender() == Gender.FEMALE && u.getAge() % 2 == 0)
            .allMatch(u -> true);                   // least overhead terminal function I can think of
    long time2 = System.currentTimeMillis();
    return time2 - time1;
}

static long test2(List<User> users){
    long time1 = System.currentTimeMillis();
    users.stream()
            .filter(u -> u.getGender() == Gender.FEMALE)
            .filter(u -> u.getAge() % 2 == 0)
            .allMatch(u -> true);                   // least overhead terminal function I can think of
    long time2 = System.currentTimeMillis();
    return time2 - time1;
}

static long test3(List<User> users){
    long time1 = System.currentTimeMillis();
    users.stream()
            .filter(((Predicate<User>) u -> u.getGender() == Gender.FEMALE).and(u -> u.getAge() % 2 == 0))
            .allMatch(u -> true);                   // least overhead terminal function I can think of
    long time2 = System.currentTimeMillis();
    return time2 - time1;
}

public static void main(String... args) {
    int size = 10000000;
    List<User> users =
    IntStream.range(0,size)
            .mapToObj(i -> i % 2 == 0 ? new User(Gender.MALE, i % 100) : new User(Gender.FEMALE, i % 100))
            .collect(Collectors.toCollection(()->new ArrayList<>(size)));
    repeat("one filter with predicate of form u -> exp1 && exp2", users, Temp::test1, 100);
    repeat("two filters with predicates of form u -> exp1", users, Temp::test2, 100);
    repeat("one filter with predicate of form predOne.and(pred2)", users, Temp::test3, 100);
}

private static void repeat(String name, List<User> users, ToLongFunction<List<User>> test, int iterations) {
    System.out.println(name + ", list size " + users.size() + ", averaged over " + iterations + " runs: " + IntStream.range(0, iterations)
            .mapToLong(i -> test.applyAsLong(users))
            .summaryStatistics());
}
Hank D
la source
3
Intéressant - lorsque je modifie l'ordre pour exécuter test2 AVANT test1, test1 s'exécute légèrement plus lentement. Ce n'est que lorsque test1 s'exécute en premier qu'il semble plus rapide. Quelqu'un peut-il reproduire cela ou avoir des idées?
Sperr
5
Cela peut être dû au fait que le coût de la compilation HotSpot est engagé par le test exécuté en premier.
DaBlick
@Sperr vous avez raison, lorsque l'ordre a changé, les résultats ne sont pas prévisibles. Mais, lorsque j'exécute cela avec trois threads différents, un filtre toujours complexe donne de meilleurs résultats, quel que soit le thread qui commence en premier. Voici les résultats. Test #1: {count=100, sum=7207, min=65, average=72.070000, max=91} Test #3: {count=100, sum=7959, min=72, average=79.590000, max=97} Test #2: {count=100, sum=8869, min=79, average=88.690000, max=110}
Paramesh Korrakuti
2

C'est le résultat des 6 combinaisons différentes de l'échantillon test partagé par @Hank D Il est évident que le prédicat de la forme u -> exp1 && exp2est très performant dans tous les cas.

one filter with predicate of form u -> exp1 && exp2, list size 10000000, averaged over 100 runs: LongSummaryStatistics{count=100, sum=3372, min=31, average=33.720000, max=47}
two filters with predicates of form u -> exp1, list size 10000000, averaged over 100 runs: LongSummaryStatistics{count=100, sum=9150, min=85, average=91.500000, max=118}
one filter with predicate of form predOne.and(pred2), list size 10000000, averaged over 100 runs: LongSummaryStatistics{count=100, sum=9046, min=81, average=90.460000, max=150}

one filter with predicate of form u -> exp1 && exp2, list size 10000000, averaged over 100 runs: LongSummaryStatistics{count=100, sum=8336, min=77, average=83.360000, max=189}
one filter with predicate of form predOne.and(pred2), list size 10000000, averaged over 100 runs: LongSummaryStatistics{count=100, sum=9094, min=84, average=90.940000, max=176}
two filters with predicates of form u -> exp1, list size 10000000, averaged over 100 runs: LongSummaryStatistics{count=100, sum=10501, min=99, average=105.010000, max=136}

two filters with predicates of form u -> exp1, list size 10000000, averaged over 100 runs: LongSummaryStatistics{count=100, sum=11117, min=98, average=111.170000, max=238}
one filter with predicate of form u -> exp1 && exp2, list size 10000000, averaged over 100 runs: LongSummaryStatistics{count=100, sum=8346, min=77, average=83.460000, max=113}
one filter with predicate of form predOne.and(pred2), list size 10000000, averaged over 100 runs: LongSummaryStatistics{count=100, sum=9089, min=81, average=90.890000, max=137}

two filters with predicates of form u -> exp1, list size 10000000, averaged over 100 runs: LongSummaryStatistics{count=100, sum=10434, min=98, average=104.340000, max=132}
one filter with predicate of form predOne.and(pred2), list size 10000000, averaged over 100 runs: LongSummaryStatistics{count=100, sum=9113, min=81, average=91.130000, max=179}
one filter with predicate of form u -> exp1 && exp2, list size 10000000, averaged over 100 runs: LongSummaryStatistics{count=100, sum=8258, min=77, average=82.580000, max=100}

one filter with predicate of form predOne.and(pred2), list size 10000000, averaged over 100 runs: LongSummaryStatistics{count=100, sum=9131, min=81, average=91.310000, max=139}
two filters with predicates of form u -> exp1, list size 10000000, averaged over 100 runs: LongSummaryStatistics{count=100, sum=10265, min=97, average=102.650000, max=131}
one filter with predicate of form u -> exp1 && exp2, list size 10000000, averaged over 100 runs: LongSummaryStatistics{count=100, sum=8442, min=77, average=84.420000, max=156}

one filter with predicate of form predOne.and(pred2), list size 10000000, averaged over 100 runs: LongSummaryStatistics{count=100, sum=8553, min=81, average=85.530000, max=125}
one filter with predicate of form u -> exp1 && exp2, list size 10000000, averaged over 100 runs: LongSummaryStatistics{count=100, sum=8219, min=77, average=82.190000, max=142}
two filters with predicates of form u -> exp1, list size 10000000, averaged over 100 runs: LongSummaryStatistics{count=100, sum=10305, min=97, average=103.050000, max=132}
Venkat Madhav
la source