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

235

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

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

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

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

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

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

Deamon
источник
57
Напишите любой код, который будет более читабельным в данной ситуации. Разница в производительности минимальна (и очень ситуативна).
Брайан Гетц
5
Забудьте о нано-оптимизации и используйте хорошо читаемый и поддерживаемый код. с потоками всегда следует использовать каждую операцию отдельно, включая фильтры.
Diablo

Ответы:

151

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

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

Но, как уже говорилось, этот вид издержек будет устранен оптимизатором HotSpot и незначителен.

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

Так что простого ответа нет.

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


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

Holger
источник
4
не код должен повторять полученный поток после каждого фильтра?
Джакарди
13
@Juan Карлос Диас: нет, потоки не работают таким образом. Читайте о «ленивой оценке»; промежуточные операции ничего не делают, они только изменяют результат работы терминала.
Хольгер
34

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

Для операций с пропускной способностью 10 элементов для небольшого массива: Массив из 10 элементов для операций с пропускной способностью 10 000 элементов: введите описание изображения здесь для операций с пропускной способностью 1 000 000 элементов: 1М элементы

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

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

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

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

саржа
источник
22

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

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
Интересно - когда я меняю порядок запуска test2 ДО test1, test1 работает немного медленнее. Только когда тест1 запускается первым, он кажется быстрее. Кто-нибудь может воспроизвести это или есть какие-то идеи?
Сперр
5
Возможно, это связано с тем, что стоимость компиляции HotSpot зависит от того, какой тест запускается первым.
DaBlick
@Sperr вы правы, когда порядок изменился, результаты не предсказуемы. Но когда я запускаю это с тремя разными потоками, всегда сложный фильтр дает лучшие результаты, независимо от того, какой поток запускается первым. Ниже приведены результаты. 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}
Парамеш Корракути
2

Это результат 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}
Венкат Мадхав
источник