Java 8-Streams: Mehrere Filter im Vergleich zu komplexen Bedingungen

234

Manchmal möchten Sie a Streammit 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 einzigen tun filter:

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

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

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

Deamon
quelle
57
Schreiben Sie den Code, der in der Situation besser lesbar ist. Der Leistungsunterschied ist minimal (und sehr situativ).
Brian Goetz
5
Vergessen Sie Nano-Optimierungen und verwenden Sie gut lesbaren und wartbaren Code. Bei Streams sollte jeder Vorgang immer separat verwendet werden, einschließlich Filter.
Diablo

Antworten:

150

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 unterschiedlich sein, aber das ist 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 einen Unterschied gibt.

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

Wie bereits erwähnt, wird diese Art von Overhead durch den HotSpot-Optimierer beseitigt und ist vernachlässigbar.

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

Es gibt also keine einfache Antwort.

Unter dem Strich sollten Sie nicht an solche Leistungsunterschiede unterhalb der Geruchserkennungsschwelle denken. Verwenden Sie, was besser lesbar ist.


¹… und würde eine Implementierung erfordern, die die parallele Verarbeitung nachfolgender Stufen durchführt, ein Weg, den die Standard-Stream-Implementierung derzeit nicht einschlägt

Holger
quelle
4
Muss der Code den resultierenden Stream nicht nach jedem Filter iterieren?
Jucardi
13
@ Juan Carlos Diaz: Nein, Streams funktionieren nicht so. Lesen Sie mehr über "faule Bewertung"; Zwischenoperationen machen nichts, sie verändern nur das Ergebnis der Terminaloperation.
Holger
31

Eine komplexe Filterbedingung ist in Bezug auf die Leistung besser, aber die beste Leistung zeigt, dass eine Schleife mit einem Standard if clausedie beste Option ist. Der Unterschied auf einem kleinen Array 10 Elemente Unterschied kann ~ 2 mal sein, für ein großes 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 Arrays 10 Elemente Durchsatz ops / s: Array mit 10 Elementen Für mittlere 10.000 Elemente Durchsatz ops / s: Geben Sie hier die Bildbeschreibung ein Für große Arrays 1.000.000 Elemente Durchsatz ops / s: 1M Elemente

HINWEIS: Tests werden fortgesetzt

  • 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

Serge
quelle
22

Dieser Test zeigt, dass Ihre zweite Option eine deutlich bessere Leistung erbringen 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());
}
Hank D.
quelle
3
Interessant - wenn ich die Reihenfolge ändere, um test2 VOR test1 auszuführen, läuft test1 etwas langsamer. Nur wenn test1 zuerst ausgeführt wird, scheint es schneller zu sein. Kann jemand dies reproduzieren oder irgendwelche Einsichten haben?
Sperr
5
Dies kann daran liegen, dass die Kosten für die HotSpot-Kompilierung durch den zuerst ausgeführten Test anfallen.
DaBlick
@Sperr Sie haben Recht, wenn sich die Reihenfolge ändert, sind die Ergebnisse nicht vorhersehbar. Wenn ich dies jedoch mit drei verschiedenen Threads ausführe, liefert ein immer komplexer Filter bessere Ergebnisse, unabhängig davon, welcher Thread zuerst startet. Unten sind die Ergebnisse. 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

Dies ist das Ergebnis der 6 verschiedenen Kombinationen des von @Hank D gemeinsam genutzten Beispieltests. Es ist offensichtlich, dass das Prädikat der Form u -> exp1 && exp2in allen Fällen sehr leistungsfähig ist.

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
quelle