Подтвердить что ты не робот

Java 8 Streams: несколько фильтров или сложное условие

Иногда вы хотите отфильтровать Stream с более чем одним условием:

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

или вы можете сделать то же самое со сложным условием и одним filter:

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

Я предполагаю, что второй подход имеет лучшие характеристики производительности, но я этого не знаю.

Первый подход выигрывает в удобочитаемости, но что лучше для производительности?

4b9b3361

Ответ 1

Код, который должен быть выполнен для обеих альтернатив, настолько похож, что вы не можете точно предсказать результат. Структура базового объекта может отличаться, но это не проблема для оптимизатора точек доступа. Таким образом, это зависит от других окружающих условий, которые приведут к более быстрому исполнению, если есть какая-либо разница.

Объединение двух экземпляров фильтра создает больше объектов и, следовательно, больше делегирует код, но это может измениться, если вы используете ссылки на методы, а не лямбда-выражения, например, замените filter(x → x.isCool()) на filter(ItemType::isCool). Таким образом, вы устранили синтетический метод делегирования, созданный для выражения лямбды. Таким образом, объединение двух фильтров с использованием двух ссылок на методы может создать один и тот же или меньший код делегирования, чем один вызов filter используя выражение лямбда с &&.

Но, как сказано, такие накладные расходы будут устранены оптимизатором HotSpot и будут незначительными.

Теоретически два фильтра могут быть проще распараллеливаться, чем один фильтр, но это относится только к довольно интенсивным вычислительным задачам¹.

Поэтому нет простого ответа.

Суть в том, что не стоит думать о таких различиях производительности ниже порога обнаружения запаха. Используйте то, что более читаемо.


¹... и потребует реализации, выполняющей параллельную обработку последующих этапов, путь, который в настоящее время не используется стандартной реализацией потока

Ответ 2

Этот тест показывает, что ваш второй вариант может работать значительно лучше. Выводы сначала, затем код:

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}

теперь код:

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

В наборе у меня есть 10 тысяч ссылок на отдельные объекты. Затем я сделал "тест скорости":

long time1 = System.currentTimeMillis();    
users.stream()
     .filter((u) -> u.getGender() == Gender.FEMALE && u.getAge() % 2 == 0);
long time2 = System.currentTimeMillis();
System.out.printf("%d milli seconds", time2 - time1);

После 10 исполнений среднее время составляло 45,5 миллисекунды. Затем я использую multi filter():

long time1 = System.currentTimeMillis();    
users.stream()
     .filter((u) -> u.getGender() == Gender.FEMALE)
     .filter((u) -> u.getAge() % 2 == 0);
long time2 = System.currentTimeMillis();
System.out.printf("%d milli seconds", time2 - time1);

Среднее время составило 44,7 миллисекунды.

Конечно, это тест с простыми операциями, но я считаю, что нет никакой разницы, потому что Stream использует внутреннюю итерацию, в отличие от циклов "за", которые используют внешнюю итерацию, другими словами, цикл происходит только один раз и после завершения результат возвращается. Вероятно, есть разница во времени, которую компилятор возьмет, чтобы прочитать фильтры, что является несущественным.

Ответ 4

Сложное условие фильтрации лучше с точки зрения производительности, но наилучшая производительность покажет устаревшую моду для цикла со стандартным if clause - это лучший вариант. Разница в малом массиве 10 элементов может составлять ~ 2 раза, для большого массива разница не так велика.
Вы можете взглянуть на мой проект GitHub, где я провел тесты производительности для нескольких вариантов итераций массива.

Для операций с пропускной способностью 10 элементов для малого массива: 10 element array Для операций с пропускной способностью 10 000 элементов в секунду: enter image description here Для операций с пропускной способностью 1 000 000 элементов и массива: 1M elements

ПРИМЕЧАНИЕ: тесты выполняются на

  • 8 CPU
  • 1 ГБ ОЗУ
  • Версия ОС: 16.04.1 LTS (Xenial Xerus)
  • Ява версия: 1.8.0_121
  • jvm: -XX: + использовать G1GC -server -Xmx1024m -Xms1024m

ОБНОВЛЕНИЕ: Java 11 имеет некоторый прогресс в производительности, но динамика остается прежней

Режим тестирования: пропускная способность, количество операций/время Java 8vs11

Ответ 5

Это результат 6 различных комбинаций примера теста, совместно используемых @Hank D. Очевидно, что предикат формы u → exp1 && exp2 во всех случаях обладает высокой производительностью.

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}