Потоки Java 8: множественные фильтры против сложного условия


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

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

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

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

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

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

2 140

2 ответа:

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

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

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

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

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

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


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

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

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