Java 8 Streams: múltiples filtros versus condición compleja

235

A veces desea filtrar un Streamcon más de una condición:

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

o podría hacer lo mismo con una condición compleja y una sola filter :

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

Supongo que el segundo enfoque tiene mejores características de rendimiento, pero no lo .

El primer enfoque gana en legibilidad, pero ¿qué es mejor para el rendimiento?

deamon
fuente
57
Escriba el código que sea más legible en la situación. La diferencia de rendimiento es mínima (y altamente situacional).
Brian Goetz
55
Olvídate de las nanooptimizaciones y usa código altamente legible y mantenible. con las transmisiones, siempre se debe usar cada operación por separado, incluidos los filtros.
Diablo

Respuestas:

151

El código que debe ejecutarse para ambas alternativas es tan similar que no puede predecir un resultado de manera confiable. La estructura del objeto subyacente puede diferir, pero eso no es un desafío para el optimizador de puntos de acceso. Por lo tanto, depende de otras condiciones circundantes que darán lugar a una ejecución más rápida, si hay alguna diferencia.

La combinación de dos instancias de filtro crea más objetos y, por lo tanto, más código de delegación, pero esto puede cambiar si usa referencias de método en lugar de expresiones lambda, por ejemplo, reemplazar filter(x -> x.isCool())por filter(ItemType::isCool). De esa manera, ha eliminado el método de delegación sintético creado para su expresión lambda. Por lo tanto, combinar dos filtros usando dos referencias de método podría crear el mismo código de delegación o menor que una sola filterinvocación usando una expresión lambda con &&.

Pero, como se dijo, este tipo de sobrecarga será eliminado por el optimizador de HotSpot y es insignificante.

En teoría, dos filtros podrían ser paralelizados más fácilmente que un solo filtro, pero eso solo es relevante para tareas intensas computacionales¹.

Entonces no hay una respuesta simple.

La conclusión es que no piense en esas diferencias de rendimiento por debajo del umbral de detección de olores. Usa lo que sea más legible.


¹ ... y requeriría una implementación que realice el procesamiento paralelo de las etapas posteriores, un camino que actualmente no es tomado por la implementación estándar de Stream

Holger
fuente
44
¿No tiene que iterar el código la secuencia resultante después de cada filtro?
jucardi el
13
@Juan Carlos Díaz: no, las transmisiones no funcionan de esa manera. Lea sobre "evaluación perezosa"; Las operaciones intermedias no hacen nada, solo alteran el resultado de la operación del terminal.
Holger
34

Una condición de filtro compleja es mejor en la perspectiva de rendimiento, pero el mejor rendimiento mostrará que if clausela opción más antigua es un bucle con un estándar . La diferencia en una matriz pequeña de 10 elementos puede ser ~ 2 veces, para una matriz grande la diferencia no es tan grande.
Puedes echar un vistazo a mi proyecto GitHub , donde hice pruebas de rendimiento para múltiples opciones de iteración de matriz

Para operaciones de rendimiento de 10 elementos de una matriz pequeña: Matriz de 10 elementos Para operaciones de rendimiento medio de 10,000 elementos / s: ingrese la descripción de la imagen aquí Para operaciones de producción de 1,000,000 elementos de una matriz grande: Elementos 1M

NOTA: las pruebas se ejecutan en

  • 8 CPU
  • 1 GB de RAM
  • Versión del sistema operativo: 16.04.1 LTS (Xenial Xerus)
  • versión de Java: 1.8.0_121
  • jvm: -XX: + UseG1GC -server -Xmx1024m -Xms1024m

ACTUALIZACIÓN: Java 11 tiene cierto progreso en el rendimiento, pero la dinámica se mantiene igual

Modo de referencia: rendimiento, operaciones / tiempo Java 8vs11

Sarga
fuente
22

Esta prueba muestra que su segunda opción puede funcionar significativamente mejor. Primero los hallazgos, luego el código:

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}

ahora el código:

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
fuente
3
Interesante: cuando cambio el orden para ejecutar test2 ANTES de test1, test1 se ejecuta un poco más lento. Es solo cuando test1 se ejecuta primero que parece más rápido. ¿Alguien puede reproducir esto o tener alguna idea?
Sperr
55
Puede ser porque el costo de la compilación de HotSpot es incurrido por cualquier prueba que se ejecute primero.
DaBlick
@Sperr tiene razón, cuando el orden cambió, los resultados no son predecibles. Pero, cuando ejecuto esto con tres subprocesos diferentes, siempre filtro complejo que da mejores resultados, independientemente de qué subproceso comience primero. Abajo están los resultados. 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

Este es el resultado de las 6 combinaciones diferentes de la prueba de muestra compartida por @Hank D Es evidente que el predicado de la forma u -> exp1 && exp2es altamente eficiente en todos los casos.

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
fuente