it-swarm.com.de

Java 8 Streams: Mehrere Filter gegen komplexe Bedingungen

Manchmal möchten Sie ein Stream mit mehr als einer Bedingung filtern:

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

oder Sie könnten dasselbe mit einer komplexen Bedingung und einer einzelnen filter tun:

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

Ich vermute, dass der zweite Ansatz bessere Leistungseigenschaften hat, aber ich weiß es nicht .

Der erste Ansatz gewinnt an Lesbarkeit, aber was ist besser für die Leistung?

193
deamon

Der Code, der für beide Alternativen ausgeführt werden muss, ist so ähnlich, dass Sie ein Ergebnis nicht zuverlässig vorhersagen können. Die zugrunde liegende Objektstruktur kann abweichen, dies ist jedoch keine Herausforderung für den Hotspot-Optimierer. Es hängt also von anderen Umgebungsbedingungen ab, die zu einer schnelleren Ausführung führen, wenn es Unterschiede gibt.

Das Kombinieren von zwei Filterinstanzen erzeugt mehr Objekte und daher mehr delegierenden Code. Dies kann sich jedoch ändern, wenn Sie Methodenreferenzen anstelle von Lambda-Ausdrücken verwenden, z. Ersetzen Sie filter(x -> x.isCool()) durch filter(ItemType::isCool). Auf diese Weise haben Sie die für Ihren Lambda-Ausdruck erstellte synthetische Delegierungsmethode entfernt. Wenn Sie also zwei Filter mit zwei Methodenreferenzen kombinieren, wird möglicherweise derselbe oder ein geringerer Delegierungscode als bei einem einzelnen filter -Aufruf mit einem Lambda-Ausdruck mit &&.

Aber wie gesagt, diese Art von Overhead wird vom HotSpot-Optimierer beseitigt und ist vernachlässigbar.

Theoretisch könnten zwei Filter einfacher parallelisiert werden als ein einzelner Filter, dies ist jedoch nur für rechenintensive Aufgaben relevant¹.

Es gibt also keine einfache Antwort.

Unter dem Strich sollten solche Leistungsunterschiede unterhalb der Geruchserkennungsschwelle nicht berücksichtigt werden. Verwenden Sie, was besser lesbar ist.


¹… und würde eine Implementierung erfordern, bei der nachfolgende Phasen parallel verarbeitet werden, ein Weg, den die Standard-Stream-Implementierung derzeit nicht geht

129
Holger

Dieser Test zeigt, dass Ihre zweite Option wesentlich bessere Ergebnisse erzielen kann. Ergebnisse zuerst, dann der 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}

jetzt der 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

Eine komplexe Filterbedingung ist in Bezug auf die Leistung besser, aber die beste Leistung zeigt die alte Mode für eine Schleife mit einem Standard if clause ist die beste Option. Der Unterschied bei einem kleinen Array mit 10 Elementen kann bis zu 2-mal sein, bei einem großen Array ist der Unterschied nicht so groß.
Sie können sich mein GitHub-Projekt ansehen, in dem ich Leistungstests für mehrere Array-Iterationsoptionen durchgeführt habe

Für kleine Array-10-Element-Durchsatzoperationen/s: 10 element array Für mittlere Durchsatzraten von 10.000 Elementen/s: enter image description here Für große Arrays mit 1.000.000 Elementen Durchsatz: 1M elements

HINWEIS: Tests laufen weiter

  • 8 CPU
  • 1 GB RAM
  • Betriebssystemversion: 16.04.1 LTS (Xenial Xerus)
  • Java-Version: 1.8.0_121
  • jvm: -XX: + UseG1GC -server -Xmx1024m -Xms1024m

UPDATE: Java 11 hat einige Fortschritte bei der Leistung, aber die Dynamik bleibt gleich

Benchmark-Modus: Durchsatz, Betrieb/Zeit Java 8vs11

5
Serge

Dies ist das Ergebnis der 6 verschiedenen Kombinationen des von @Hank D geteilten Beispieltests. Es ist offensichtlich, dass das Prädikat der Form u -> exp1 && exp2 ist in allen Fällen sehr performant.

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