Java 8 Streams: vários filtros vs. condição complexa


235

Às vezes, você deseja filtrar um Streamcom mais de uma condição:

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

ou você pode fazer o mesmo com uma condição complexa e uma única filter :

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

Meu palpite é que a segunda abordagem tem melhores características de desempenho, mas eu não sei .

A primeira abordagem ganha em legibilidade, mas o que é melhor para o desempenho?


57
Escreva o código que for mais legível na situação. A diferença de desempenho é mínima (e altamente situacional).
Brian Goetz

5
Esqueça as nano-otimizações e use código altamente legível e de manutenção. com fluxos, deve-se sempre usar cada operação separadamente, incluindo filtros.
Diablo

Respostas:


151

O código que deve ser executado para ambas as alternativas é tão semelhante que você não pode prever um resultado com confiabilidade. A estrutura do objeto subjacente pode ser diferente, mas isso não é um desafio para o otimizador de hotspot. Portanto, depende de outras condições circundantes que renderão uma execução mais rápida, se houver alguma diferença.

A combinação de duas instâncias de filtro cria mais objetos e, portanto, mais código de delegação, mas isso pode mudar se você usar referências de método em vez de expressões lambda, por exemplo, substituir filter(x -> x.isCool())por filter(ItemType::isCool). Dessa forma, você eliminou o método de delegação sintético criado para sua expressão lambda. Portanto, combinar dois filtros usando duas referências de método pode criar o mesmo ou menor código de delegação do que uma única filterchamada usando uma expressão lambda com &&.

Mas, como dito, esse tipo de sobrecarga será eliminado pelo otimizador HotSpot e é insignificante.

Em teoria, dois filtros poderiam ser mais facilmente paralelizados do que um único filtro, mas isso é relevante apenas para tarefas intensas computacionais¹.

Portanto, não há uma resposta simples.

A conclusão é que não pense nessas diferenças de desempenho abaixo do limite de detecção de odores. Use o que for mais legível.


¹… e exigiria uma implementação que processe paralelamente os estágios subsequentes, uma estrada atualmente não tomada pela implementação padrão do Stream


4
o código não precisa iterar o fluxo resultante após cada filtro?
jucardi

13
@ Juan Carlos Diaz: não, os fluxos não funcionam dessa maneira. Leia sobre "avaliação preguiçosa"; operações intermediárias não fazem nada, apenas alteram o resultado da operação do terminal.
Holger

34

Uma condição de filtro complexa é melhor na perspectiva do desempenho, mas o melhor desempenho mostra a moda antiga para loop com um padrão if clauseé a melhor opção. A diferença em uma matriz pequena A diferença de 10 elementos pode ~ 2 vezes, para uma matriz grande a diferença não é tão grande.
Você pode dar uma olhada no meu projeto GitHub , onde fiz testes de desempenho para várias opções de iteração de matriz

Para ops / s de taxa de transferência de 10 elementos de matriz pequena: Matriz de 10 elementos Para ops / s de taxa de transferência de 10.000 elementos médios : Para ops / s de taxa de transferência de 10.000 elementos médios insira a descrição da imagem aqui : 1 milhão de elementos

NOTA: os testes são executados em

  • 8 CPU
  • 1 GB de RAM
  • Versão do SO: 16.04.1 LTS (Xenial Xerus)
  • versão java: 1.8.0_121
  • jvm: -XX: + UseG1GC -server -Xmx1024m -Xms1024m

UPDATE: Java 11 tem algum progresso no desempenho, mas a dinâmica permanece a mesma

Modo de referência: taxa de transferência, ops / time Java 8vs11


22

Este teste mostra que sua segunda opção pode ter um desempenho significativamente melhor. Resultados primeiro, depois o 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}

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

3
Interessante - quando mudo a ordem para executar o teste2 ANTES do teste1, o teste1 é executado um pouco mais devagar. É somente quando test1 é executado primeiro que parece mais rápido. Alguém pode reproduzir isso ou ter alguma ideia?
Sperr

5
Pode ser porque o custo da compilação do HotSpot é incorrido por qualquer teste executado primeiro.
DaBlick

@ Perper você está certo, quando a ordem mudou, os resultados não são previsíveis. Mas, quando eu executo isso com três threads diferentes, o filtro sempre complexo dá melhores resultados, independentemente de qual thread iniciar primeiro. Abaixo estão os 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 28/04

2

Este é o resultado das 6 combinações diferentes do teste de amostra compartilhado pelo @Hank D É evidente que o predicado de forma u -> exp1 && exp2é de alto desempenho em todos os 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}
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.