web-dev-qa-db-fra.com

Java 8 Streams: filtres multiples vs condition complexe

Parfois, vous souhaitez filtrer une Stream avec plusieurs conditions:

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

ou vous pouvez faire la même chose avec une condition complexe et un simple 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 la connais pas .

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

193
deamon

Le code à exécuter pour les deux alternatives est tellement similaire que vous ne pouvez pas prédire un résultat de manière fiable. La structure de l’objet sous-jacent peut différer, mais ce n’est pas un défi pour l’optimiseur de hotspot. Donc, cela dépend des autres conditions environnantes qui permettront une exécution plus rapide, s’il ya une différence.

La combinaison de deux occurrences de filtre crée plus d'objets et donc plus de code délégant, 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 un code de délégation identique ou inférieur à celui d'une seule invocation filter à l'aide d'une expression lambda avec &&.

Mais, comme cela a été dit, ce genre de frais généraux sera éliminé par l’optimiseur HotSpot et est négligeable.

En théorie, il serait plus facile de mettre en parallèle deux filtres qu’un seul filtre, mais cela n’est pertinent que pour des tâches plutôt intensives en calcul¹.

Donc, il n'y a pas de réponse simple.

En bout de ligne, ne pensez pas à de telles 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 effectuant un traitement parallèle des étapes ultérieures, voie actuellement non prise en charge par l'implémentation Stream standard

129
Holger

Ce test montre que votre deuxième option peut fonctionner beaucoup mieux. Les constatations 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());
}
19
Hank D

Une condition de filtre complexe est meilleure du point de vue des performances, mais les meilleures performances reprennent l’ancienne méthode de boucle avec un if clause standard: la meilleure option. La différence sur un petit tableau La différence de 10 éléments peut être environ 2 fois. Pour un grand tableau, la différence n’est pas si grande.
Vous pouvez jeter un coup d’œil sur mon projet GitHub , où j’ai fait des tests de performance pour plusieurs options d’itération de tableau.

Pour un petit tableau, débit de 10 éléments/s: 10 element array Pour un débit moyen de 10 000 éléments op/s: enter image description here Pour un grand tableau 1.000.000 éléments débit ops/s: 1M elements

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

UPDATE: Java 11 a quelques progrès sur les performances, mais la dynamique reste la même.

Mode de référence: débit, ops/time Java 8vs11

5
Serge

Ceci est le résultat des 6 combinaisons différentes de l'exemple de test partagé par @Hank D Il est évident que le prédicat de forme u -> exp1 && exp2 est 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}
1
Venkat Madhav