Java 8 Streams: filtri multipli vs. condizioni complesse


235

A volte vuoi filtrare un Streamcon più di una condizione:

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

oppure potresti fare lo stesso con una condizione complessa e una singola filter :

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

La mia ipotesi è che il secondo approccio abbia migliori caratteristiche prestazionali, ma non lo so .

Il primo approccio vince in leggibilità, ma cosa c'è di meglio per le prestazioni?


57
Scrivi il codice più leggibile nella situazione. La differenza di prestazioni è minima (e altamente situazionale).
Brian Goetz,

5
Dimentica le nano-ottimizzazioni e usa un codice altamente leggibile e gestibile. con i flussi, si dovrebbe sempre usare ciascuna operazione separatamente, inclusi i filtri.
Diablo,

Risposte:


151

Il codice che deve essere eseguito per entrambe le alternative è così simile che non è possibile prevedere un risultato in modo affidabile. La struttura dell'oggetto sottostante potrebbe differire, ma ciò non rappresenta una sfida per l'ottimizzatore di hotspot. Quindi dipende da altre condizioni circostanti che porteranno a un'esecuzione più veloce, se c'è qualche differenza.

La combinazione di due istanze di filtro crea più oggetti e quindi più delega del codice, ma ciò può cambiare se si utilizzano riferimenti a metodi anziché espressioni lambda, ad esempio sostituisci filter(x -> x.isCool())con filter(ItemType::isCool). In questo modo hai eliminato il metodo di delega sintetico creato per la tua espressione lambda. Pertanto, la combinazione di due filtri utilizzando due riferimenti di metodo potrebbe creare lo stesso o un codice di delega inferiore rispetto a una singola filterchiamata usando un'espressione lambda con &&.

Ma, come detto, questo tipo di overhead sarà eliminato dall'ottimizzatore HotSpot ed è trascurabile.

In teoria, due filtri potrebbero essere più facilmente parallelizzati rispetto a un singolo filtro, ma ciò è rilevante solo per compiti intensi piuttosto computazionali¹.

Quindi non esiste una risposta semplice.

La linea di fondo è, non pensare a tali differenze di prestazioni al di sotto della soglia di rilevamento degli odori. Usa ciò che è più leggibile.


¹ ... e richiederebbe un'implementazione che esegua l'elaborazione parallela delle fasi successive, una strada attualmente non presa dall'implementazione standard di Stream


4
il codice non deve ripetere il flusso risultante dopo ogni filtro?
jucardi,

13
@Juan Carlos Diaz: no, i flussi non funzionano in questo modo. Leggi "valutazione pigra"; le operazioni intermedie non fanno nulla, alterano solo il risultato dell'operazione terminale.
Holger,

34

Una condizione di filtro complessa è migliore in termini di prestazioni, ma la migliore prestazione mostrerà la vecchia moda per il loop con uno standard if clauseè l'opzione migliore. La differenza su una piccola differenza di 10 elementi può essere ~ 2 volte, per una grande matrice la differenza non è così grande.
Puoi dare un'occhiata al mio progetto GitHub , in cui ho eseguito test delle prestazioni per più opzioni di iterazione di array

Per op / s di throughput di piccoli elementi 10 array: Matrice di 10 elementi Per op / s di throughput di medio 10.000 elementi: inserisci qui la descrizione dell'immagine Per op / s di throughput di grandi array di 1.000.000 elementi: 1 milione di elementi

NOTA: i test vengono eseguiti

  • 8 CPU
  • 1 GB di RAM
  • Versione SO: 16.04.1 LTS (Xenial Xerus)
  • versione java: 1.8.0_121
  • jvm: -XX: + UsaG1GC -server -Xmx1024m -Xms1024m

AGGIORNAMENTO: Java 11 presenta alcuni progressi nelle prestazioni, ma le dinamiche rimangono invariate

Modalità benchmark: velocità effettiva, operazioni / tempo Java 8vs11


22

Questo test mostra che la tua seconda opzione può offrire prestazioni significativamente migliori. Prima i risultati, quindi il codice:

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}

ora il codice:

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());
}

3
Interessante: quando cambio l'ordine per eseguire test2 PRIMA di test1, test1 funziona leggermente più lentamente. È solo quando test1 viene eseguito per primo che sembra più veloce. Qualcuno può riprodurlo o avere approfondimenti?
Sperr,

5
Potrebbe essere perché il costo della compilazione di HotSpot è sostenuto da qualunque test venga eseguito per primo.
DaBlick

@Sperr hai ragione, quando l'ordine è cambiato, i risultati non sono prevedibili. Ma quando eseguo questo con tre thread diversi, filtro sempre complesso che dà risultati migliori, indipendentemente da quale thread inizia per primo. Di seguito sono riportati i risultati. 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

Questo è il risultato delle 6 diverse combinazioni del test di esempio condivise da @Hank D È evidente che il predicato della forma u -> exp1 && exp2è altamente performante in tutti i casi.

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}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.