TransWikia.com

Por que existe tanta diferença de performance entre stream e loops normais?

Stack Overflow em Português Asked by nullptr on December 31, 2020

Estava lendo um artigo relacionado à performance das streams/loops, e me assustei com a diferença de performance entre a utilização de loops em relação à grandes quantidades de dados.

Resolvi realizar um teste rápido utilizando o seguinte código:

public static void main(String[] args) {
    final int limite = 10_000;

    long inicioFor = System.currentTimeMillis();

    for(int i = 0; i < limite; i++) {
        System.out.println(i);
    }

    long terminoFor = System.currentTimeMillis();

    long inicioStream = System.currentTimeMillis();

    IntStream.range(0, limite).forEach(System.out::println);

    long terminoStream = System.currentTimeMillis();

    System.out.println();
    System.out.println("Usando for: " + (terminoFor - inicioFor) );
    System.out.println("Usando stream: " + (terminoStream - inicioStream) );
}

É um código relativamente simples, porém notei uma coisa interessante:

  1. Quando o limite é definido com valor 10_000, o tempo resultante é o seguinte:

Usando for: 54
Usando stream: 72

  1. Quando o limite é definido com valor 1_000_000, a stream é relativamente mais rápida:

Usando for: 4314
Usando stream: 4202

Entendo que este é um teste simples, sem utilização de ferramentas de benchmark entre outras métricas.

Gostaria de saber o porquê da existência desta diferença de tempo no processamento utilizando streams e loops, posso não estar correto, mas parece que as streams performam melhor quanto maior a quantidade de dados (caminhando para a questão das streams infinitas).


Complementando com algumas informações colocadas pelo @Victor, tive resultados extremamente diferentes retirando as operações de IO:

  1. Quando o limite é definido com valor 10_000:

Usando for: 0
Usando stream: 36

  1. Quando o limite é definido com valor 1_000_000:

Usando for: 2
Usando stream: 43

Conforme apontado também por @Maniero, a razão se deve ser o alto custo para fazer a infraestrutura funcionar de acordo.

2 Answers

Acabei de responder sobre isto (O LINQ é o stream do C#). O stream é uma abstração, é uma camada a mais para executar, há um custo para fazer esta infraestrutura funcionar, há chamadas de função onde não deveria ocorrer no puro, há indireções, tudo isso tem custo, então ele precisa ser mais caro que a execução pura e simples de um laço. Está executando mais coisas, tem que ser mais lento.

O stream é um método que tem um laço dentro dele e que executa um método dentro dele de forma indireta, neste exemplo é o System.out::println. Que por sinal não é uma boa forma porque ele é IO e o custo disso é tão alto que você está medindo algo como 98% do tempo o IO e não o mecanismo do laço ou do stream. Em algo que custa pouco a diferença é muito maior.

Até me impressiona a excelente otimização que o compilador Java faz, mas o teste onde dá o for mais lento certamente pegou alguma interferência, não tem como ele ser mais lento. Certamente em grande volume o custo da infraestrutura pode não impactar tanto, mas não pode ser mais rápido. E por isso que dá uma discrepância entre o seu resultado e do Victor, o dele executa muitas vezes que diminui a influência da infra do stream.

Correct answer by Maniero on December 31, 2020

Bem, fiz esse teste:

import java.util.stream.LongStream;

class Teste {
    public static void main(String[] args) {
        var limite = 50_000_000_000L;
        var x = new long[1];

        var inicioStream1 = System.currentTimeMillis();
        LongStream.range(0, limite).forEach(z -> x[0] = z);
        var terminoStream1 = System.currentTimeMillis();

        var inicioFor1 = System.currentTimeMillis();
        for (var i = 0L; i < limite; i++) {
            x[0] = i;
        }
        var terminoFor1 = System.currentTimeMillis();

        var inicioStream2 = System.currentTimeMillis();
        LongStream.range(0, limite).forEach(z -> x[0] = z);
        var terminoStream2 = System.currentTimeMillis();

        var inicioFor2 = System.currentTimeMillis();
        for (var i = 0L; i < limite; i++) {
            x[0] = i;
        }
        var terminoFor2 = System.currentTimeMillis();

        System.out.println();
        System.out.println("Usando for: " + (terminoFor1 - inicioFor1));
        System.out.println("Usando stream: " + (terminoStream1 - inicioStream1));
        System.out.println("Usando for outra vez: " + (terminoFor2 - inicioFor2));
        System.out.println("Usando stream outra vez: " + (terminoStream2 - inicioStream2));
    }
}

O resultado foi esse:

Usando for: 15169
Usando stream: 18422
Usando for outra vez: 15140
Usando stream outra vez: 18498

Quando você for fazer um bechmark de alguma coisa, lembre-se que o IO para o console é uma operação custosa. Isso significa que você nunca deve usar System.out.println ou System.out::println dentro do código que você está avaliando, porque ele vai interferir bastante com a performance daquilo que você está avaliando.

Acreditar que o System.out.println tem um custo de desempenho zero ou desprezível é um erro comum.

Answered by Victor Stafusa on December 31, 2020

Add your own answers!

Ask a Question

Get help from others!

© 2024 TransWikia.com. All rights reserved. Sites we Love: PCI Database, UKBizDB, Menu Kuliner, Sharing RPP