Sou iniciante no Java 8. Ainda não conheço a API em profundidade, mas fiz uma pequena referência informal para comparar o desempenho da nova API do Streams com as boas e antigas coleções.
O teste consiste em filtrar uma lista de Integer
, e para cada número par, calcular a raiz quadrada e armazená-la no resultado List
de Double
.
Aqui está o código:
public static void main(String[] args) {
//Calculating square root of even numbers from 1 to N
int min = 1;
int max = 1000000;
List<Integer> sourceList = new ArrayList<>();
for (int i = min; i < max; i++) {
sourceList.add(i);
}
List<Double> result = new LinkedList<>();
//Collections approach
long t0 = System.nanoTime();
long elapsed = 0;
for (Integer i : sourceList) {
if(i % 2 == 0){
result.add(Math.sqrt(i));
}
}
elapsed = System.nanoTime() - t0;
System.out.printf("Collections: Elapsed time:\t %d ns \t(%f seconds)%n", elapsed, elapsed / Math.pow(10, 9));
//Stream approach
Stream<Integer> stream = sourceList.stream();
t0 = System.nanoTime();
result = stream.filter(i -> i%2 == 0).map(i -> Math.sqrt(i)).collect(Collectors.toList());
elapsed = System.nanoTime() - t0;
System.out.printf("Streams: Elapsed time:\t\t %d ns \t(%f seconds)%n", elapsed, elapsed / Math.pow(10, 9));
//Parallel stream approach
stream = sourceList.stream().parallel();
t0 = System.nanoTime();
result = stream.filter(i -> i%2 == 0).map(i -> Math.sqrt(i)).collect(Collectors.toList());
elapsed = System.nanoTime() - t0;
System.out.printf("Parallel streams: Elapsed time:\t %d ns \t(%f seconds)%n", elapsed, elapsed / Math.pow(10, 9));
}.
E aqui estão os resultados para uma máquina com núcleo duplo:
Collections: Elapsed time: 94338247 ns (0,094338 seconds)
Streams: Elapsed time: 201112924 ns (0,201113 seconds)
Parallel streams: Elapsed time: 357243629 ns (0,357244 seconds)
Para este teste específico, os fluxos são duas vezes mais lentos que as coleções e o paralelismo não ajuda (ou estou usando da maneira errada?).
Questões:
- Este teste é justo? Eu cometi algum erro?
- Os fluxos são mais lentos que as coleções? Alguém fez uma boa referência formal sobre isso?
- Qual abordagem devo buscar?
Resultados atualizados.
Fiz o teste 1k vezes após o aquecimento da JVM (1k iterações), conforme recomendado por @pveentjer:
Collections: Average time: 206884437,000000 ns (0,206884 seconds)
Streams: Average time: 98366725,000000 ns (0,098367 seconds)
Parallel streams: Average time: 167703705,000000 ns (0,167704 seconds)
Nesse caso, os fluxos são mais eficientes. Gostaria de saber o que seria observado em um aplicativo em que a função de filtragem é chamada apenas uma ou duas vezes durante o tempo de execução.
fonte
IntStream
?toList
deve ser executado em paralelo, mesmo que esteja coletando em uma lista que não seja segura para threads, pois os diferentes threads serão coletados em listas intermediárias restritas a threads antes de serem mesclados.Respostas:
Pare de usar
LinkedList
para remover qualquer coisa que não seja pesada no meio da lista usando o iterador.Pare de escrever o código de benchmarking manualmente, use JMH .
Benchmarks adequados:
Resultado:
Assim como eu esperava, a implementação do fluxo é bastante mais lenta. O JIT é capaz de incorporar todas as coisas lambda, mas não produz um código tão conciso quanto a versão vanilla.
Geralmente, os fluxos Java 8 não são mágicos. Eles não podiam acelerar as coisas já bem implementadas (com, provavelmente, iterações simples ou instruções do Java 5 para cada uma delas substituídas por
Iterable.forEach()
eCollection.removeIf()
chamadas). Os fluxos são mais sobre codificação de conveniência e segurança. Conveniência - o tradeoff de velocidade está funcionando aqui.fonte
@Benchmark
vez de@GenerateMicroBenchmark
1) Você vê o tempo em menos de 1 segundo usando seu benchmark. Isso significa que pode haver forte influência de efeitos colaterais nos seus resultados. Então, eu ampliei sua tarefa 10 vezes
e executou seu benchmark. Meus resultados:
sem os
int max = 1_000_000
resultados edit ( ) foramÉ como seus resultados: o fluxo é mais lento que a coleta. Conclusão: muito tempo foi gasto para a inicialização do fluxo / transmissão de valores.
2) Depois de aumentar o fluxo de tarefas, ficou mais rápido (tudo bem), mas o fluxo paralelo permaneceu muito lento. O que há de errado? Nota: você tem
collect(Collectors.toList())
em seu comando. A coleta para uma coleção única introduz essencialmente gargalos e sobrecarga de desempenho em caso de execução simultânea. É possível estimar o custo relativo das despesas gerais substituindoPara fluxos, isso pode ser feito por
collect(Collectors.counting())
. Eu obtive resultados:Isso é uma grande tarefa! (
int max = 10000000
) Conclusão: a coleta de itens para coleta levou a maior parte do tempo. A parte mais lenta é adicionar à lista. BTW, simplesArrayList
é usado paraCollectors.toList()
.fonte
collect(Collectors.toList())
seu comando, ou seja , pode haver uma situação em que você precise resolver uma única coleção por vários threads. " Tenho quase certeza de quetoList
coletamos várias instâncias de lista diferentes em paralelo. Somente na última etapa da coleção, os elementos são transferidos para uma lista e, em seguida, retornados. Portanto, não deve haver sobrecarga de sincronização. É por isso que os colecionadores têm função de fornecedor, contador e combinador. (Poderia ser lento por outros motivos, é claro.) #collect
implementação aqui. Mas, no final, várias listas devem ser mescladas em uma única, e parece que a mesclagem é a operação mais pesada do exemplo.Mudei um pouco o código, corri no meu mac book pro que tem 8 núcleos, obtive um resultado razoável:
Coleções: Tempo decorrido: 1522036826 ns (1,522037 segundos)
Fluxos: Tempo decorrido: 4315833719 ns (4.315834 segundos)
Fluxos paralelos: Tempo decorrido: 261152901 ns (0,261153 segundos)
fonte
Para o que você está tentando fazer, eu não usaria APIs regulares de qualquer maneira. Há uma tonelada de boxe / unboxing acontecendo, então há uma enorme sobrecarga de desempenho.
Pessoalmente, acho que muitas APIs projetadas são uma porcaria, porque criam uma grande quantidade de lixo de objetos.
Tente usar matrizes primitivas de double / int e tente fazer um único encadeamento e veja qual é o desempenho.
PS: Você pode dar uma olhada no JMH para cuidar do benchmark. Ele cuida de algumas das armadilhas típicas, como o aquecimento da JVM.
fonte