Strumienie Java 8: wiele filtrów vs. złożony stan

235

Czasami chcesz filtrować Streamz więcej niż jednym warunkiem:

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

lub możesz zrobić to samo ze złożonym warunkiem i jednym filter :

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

Domyślam się, że drugie podejście ma lepszą charakterystykę wydajności, ale nie wiem .

Pierwsze podejście wygrywa w czytelności, ale co jest lepsze dla wydajności?

deamon
źródło
57
Napisz dowolny kod, który jest bardziej czytelny w danej sytuacji. Różnica w wydajności jest minimalna (i wysoce sytuacyjna).
Brian Goetz
5
Zapomnij o nanooptymalizacjach i użyj wysoce czytelnego i łatwego do utrzymania kodu. w przypadku strumieni zawsze należy używać każdej operacji osobno, łącznie z filtrami.
Diablo

Odpowiedzi:

151

Kod, który należy wykonać dla obu alternatyw, jest tak podobny, że nie można w wiarygodny sposób przewidzieć wyniku. Podstawowa struktura obiektu może się różnić, ale nie stanowi to wyzwania dla optymalizatora hotspotów. To zależy od innych warunków otoczenia, które ulegną szybszemu wykonaniu, jeśli będzie jakaś różnica.

Połączenie dwóch instancji filtra tworzy więcej obiektów, a tym samym więcej kodu delegującego, ale może się to zmienić, jeśli użyjesz referencji metod zamiast wyrażeń lambda, np. Zamień filter(x -> x.isCool())na filter(ItemType::isCool). W ten sposób wyeliminowano syntetyczną metodę delegowania utworzoną dla wyrażenia lambda. Zatem połączenie dwóch filtrów przy użyciu dwóch odwołań do metod może stworzyć ten sam lub mniejszy kod delegacji niż pojedyncze filterwywołanie przy użyciu wyrażenia lambda z &&.

Ale, jak powiedziano, tego rodzaju koszty ogólne zostaną wyeliminowane przez optymalizator HotSpot i są nieistotne.

Teoretycznie dwa filtry mogłyby być łatwiejsze do zrównoleglenia niż pojedynczy filtr, ale dotyczy to tylko intensywnych obliczeniowych zadań¹.

Więc nie ma prostej odpowiedzi.

Podsumowując, nie myśl o takich różnicach wydajności poniżej progu wykrywania zapachu. Użyj tego, co jest bardziej czytelne.


¹… i wymagałoby implementacji wykonującej równoległe przetwarzanie kolejnych etapów, która obecnie nie jest realizowana przez standardowe wdrożenie Stream

Holger
źródło
4
czy kod nie musi iterować strumienia wynikowego po każdym filtrze?
jucardi,
13
@Juan Carlos Diaz: nie, strumienie nie działają w ten sposób. Przeczytaj o „leniwej ocenie”; operacje pośrednie nic nie robią, zmieniają jedynie wynik operacji terminalowej.
Holger
34

Złożony warunek filtrowania jest lepszy z punktu widzenia wydajności, ale najlepsza wydajność pokaże starą modę dla pętli ze standardem if clausejest najlepszą opcją. Różnica na małej 10-elementowej różnicy może być ~ 2 razy, dla dużej tablicy różnica nie jest tak duża.
Możesz rzucić okiem na mój projekt GitHub , w którym przeprowadziłem testy wydajności dla wielu opcji iteracji macierzy

Dla małych układów 10-elementowa przepustowość operacji / s: Tablica 10 elementów Dla średnich 10 000 elementów-przepustowość operacji / s: wprowadź opis zdjęcia tutaj Dla dużych układów 1 000 000 elementów przepustowość operacji / s: 1 mln elementów

UWAGA: testy trwają

  • 8 procesorów
  • 1 GB pamięci RAM
  • Wersja systemu operacyjnego: 16.04.1 LTS (Xenial Xerus)
  • wersja Java: 1.8.0_121
  • jvm: -XX: + UseG1GC -server -Xmx1024m -Xms1024m

AKTUALIZACJA: Java 11 ma pewien postęp w wydajności, ale dynamika pozostaje taka sama

Tryb testu: Przepustowość, operacje / czas Java 8vs11

Serge
źródło
22

Ten test pokazuje, że Twoja druga opcja może działać znacznie lepiej. Najpierw ustalenia, a następnie kod:

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}

teraz kod:

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.
źródło
3
Ciekawe - kiedy zmieniam kolejność uruchamiania test2 PRZED test1, test1 działa nieco wolniej. Tylko gdy test1 uruchamia się jako pierwszy, wydaje się szybszy. Czy ktoś może to odtworzyć lub mieć wgląd w to?
Sperr
5
Może to być spowodowane tym, że koszt kompilacji HotSpot jest ponoszony przez dowolny test uruchamiany jako pierwszy.
DaBlick,
@ Masz rację, gdy zamówienie uległo zmianie, wyniki nie są przewidywalne. Ale kiedy uruchamiam to z trzema różnymi wątkami, zawsze złożony filtr daje lepsze wyniki, niezależnie od tego, który wątek zaczyna się jako pierwszy. Poniżej znajdują się wyniki. 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

Jest to wynik 6 różnych kombinacji przykładowego testu udostępnionego przez @Hank D. Jest oczywiste, że predykat formy u -> exp1 && exp2jest bardzo wydajny we wszystkich przypadkach.

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
źródło