Desempenho do Scala comparado ao Java

41

Antes de mais, gostaria de deixar claro que essa não é uma questão de linguagem X versus linguagem Y para determinar qual é a melhor.

Uso Java há muito tempo e pretendo continuar usando-o. Paralelamente, atualmente estou aprendendo Scala com grande interesse: além de pequenas coisas que levam algum tempo para me acostumar com a minha impressão, eu posso realmente trabalhar muito bem nesse idioma.

Minha pergunta é: como o software escrito em Scala se compara ao software escrito em Java em termos de velocidade de execução e consumo de memória? Obviamente, essa é uma pergunta difícil de responder em geral, mas eu esperaria que construções de nível superior, como correspondência de padrões, funções de ordem superior, etc., introduzissem alguma sobrecarga.

No entanto, minha experiência atual no Scala é limitada a pequenos exemplos com menos de 50 linhas de código e, até o momento, não executei nenhum benchmark. Então, eu não tenho dados reais.

Se o Scala tiver algum Java sobrecarregado, faz sentido misturar projetos Scala / Java, onde se codifica as partes mais complexas do Scala e as partes críticas de desempenho em Java? Isto é uma prática comum?

EDIT 1

Eu executei uma pequena referência: crie uma lista de números inteiros, multiplique cada número inteiro por dois e coloque-o em uma nova lista, imprima a lista resultante. Eu escrevi uma implementação Java (Java 6) e uma implementação Scala (Scala 2.9). Eu executei ambos no Eclipse Indigo no Ubuntu 10.04.

Os resultados são comparáveis: 480 ms para Java e 493 ms para Scala (média de mais de 100 iterações). Aqui estão os trechos que eu usei.

// Java
public static void main(String[] args)
{
    long total = 0;
    final int maxCount = 100;
    for (int count = 0; count < maxCount; count++)
    {
        final long t1 = System.currentTimeMillis();

        final int max = 20000;
        final List<Integer> list = new ArrayList<Integer>();
        for (int index = 1; index <= max; index++)
        {
            list.add(index);
        }

        final List<Integer> doub = new ArrayList<Integer>();
        for (Integer value : list)
        {
            doub.add(value * 2);
        }

        for (Integer value : doub)
        {
            System.out.println(value);
        }

        final long t2 = System.currentTimeMillis();

        System.out.println("Elapsed milliseconds: " + (t2 - t1));
        total += t2 - t1;
    }

    System.out.println("Average milliseconds: " + (total / maxCount));
}

// Scala
def main(args: Array[String])
{
    var total: Long = 0
    val maxCount    = 100
    for (i <- 1 to maxCount)
    {
        val t1   = System.currentTimeMillis()
        val list = (1 to 20000) toList
        val doub = list map { n: Int => 2 * n }

        doub foreach ( println )

        val t2 = System.currentTimeMillis()

        println("Elapsed milliseconds: " + (t2 - t1))
        total = total + (t2 - t1)
    }

    println("Average milliseconds: " + (total / maxCount))
}

Portanto, neste caso, parece que a sobrecarga do Scala (usando range, map, lambda) é realmente mínima, o que não está longe das informações fornecidas pelo World Engineer.

Talvez haja outras construções Scala que devam ser usadas com cuidado, porque são particularmente difíceis de executar?

EDIT 2

Alguns de vocês apontaram que as impressões nos loops internos ocupam a maior parte do tempo de execução. Eu os removi e defina o tamanho das listas como 100000 em vez de 20000. A média resultante foi de 88 ms para Java e 49 ms para Scala.

Giorgio
fonte
5
Imagino que, uma vez que o Scala compila com o código de bytes da JVM, o desempenho possa ser teoricamente equivalente ao Java executando na mesma JVM, sendo todas as outras coisas iguais. A diferença que eu acho está na maneira como o compilador Scala cria o código de bytes e se ele o faz com eficiência.
maple_shaft
2
@ maple_shaft: Ou talvez haja sobrecarga no tempo de compilação do Scala?
FrustratedWithFormsDesigner
1
@ Giorgio Não há distinção de tempo de execução entre objetos Scala e objetos Java, todos eles são objetos da JVM que são definidos e se comportam de acordo com o código de bytes. O Scala, por exemplo, como uma linguagem, tem o conceito de encerramentos, mas quando são compilados, são compilados para várias classes com código de bytes. Teoricamente, eu poderia escrever fisicamente código Java que pudesse compilar exatamente o mesmo código de bytes e o comportamento em tempo de execução seria exatamente o mesmo.
maple_shaft
2
@ maple_shaft: É exatamente isso que pretendo: acho o código Scala acima muito mais conciso e legível do que o código Java correspondente. Eu só estava pensando se faria sentido escrever partes de um projeto Scala em Java por razões de desempenho e quais seriam essas partes.
Giorgio
2
O tempo de execução será amplamente ocupado pelas chamadas println. Você precisa de um teste mais intensivo em computação.
kevin Cline

Respostas:

39

Há uma coisa que você pode fazer de forma concisa e eficiente em Java que não pode no Scala: enumerações. Para todo o resto, mesmo para construções que são lentas na biblioteca do Scala, você pode obter versões eficientes trabalhando no Scala.

Portanto, na maioria das vezes, você não precisa adicionar Java ao seu código. Mesmo para o código que usa enumeração em Java, geralmente há uma solução adequada ou boa no Scala - eu coloco a exceção em enumerações que possuem métodos extras e cujos valores int constantes são usados.

Quanto ao que observar, aqui estão algumas coisas.

  • Se você usar o padrão enriquecer minha biblioteca, sempre converta para uma classe. Por exemplo:

    // WRONG -- the implementation uses reflection when calling "isWord"
    implicit def toIsWord(s: String) = new { def isWord = s matches "[A-Za-z]+" }
    
    // RIGHT
    class IsWord(s: String) { def isWord = s matches "[A-Za-z]+" }
    implicit def toIsWord(s: String): IsWord = new IsWord(s)
    
  • Seja cauteloso com os métodos de coleta - porque eles são polimórficos na maior parte, a JVM não os otimiza. Você não precisa evitá-los, mas preste atenção nas seções críticas. Esteja ciente de que o forScala é implementado por meio de chamadas de método e classes anônimas.

  • Se estiver usando uma classe Java, como String, Arrayou AnyValclasses que correspondem às primitivas Java, prefira os métodos fornecidos por Java quando existirem alternativas. Por exemplo, use lengthem Stringe em Arrayvez de size.

  • Evite o uso descuidado de conversões implícitas, pois você pode usar conversões por engano e não por design.

  • Estender classes em vez de características. Por exemplo, se você estiver estendendo Function1, estenda AbstractFunction1.

  • Use -optimisee especialização para obter o máximo do Scala.

  • Entenda o que está acontecendo: javapé seu amigo e um monte de bandeiras Scala que mostram o que está acontecendo.

  • Os idiomas Scala são projetados para melhorar a correção e tornar o código mais conciso e sustentável. Eles não foram projetados para velocidade, portanto, se você precisar usá-lo em nullvez de Optionum caminho crítico, faça isso! Há uma razão pela qual Scala é multiparadigma.

  • Lembre-se de que a verdadeira medida de desempenho está executando o código. Veja esta pergunta para um exemplo do que pode acontecer se você ignorar essa regra.

Daniel C. Sobral
fonte
1
+1: Muita informação útil, mesmo sobre tópicos que ainda tenho que aprender, mas é útil ter lido algumas dicas antes de poder examiná-las.
Giorgio
Por que a primeira abordagem usa reflexão? De qualquer maneira, gera uma classe anônima. Por que não usá-la em vez de refletir?
JavaScript é necessário
@ Classe Oleksandr.Bezhan Anonymous é um conceito Java, não um conceito Scala. Ele gera um refinamento de tipo. Um método de classe anônima que não substitui sua classe base não pode ser acessado de fora. O mesmo não se aplica aos refinamentos de tipo do Scala, portanto, a única maneira de chegar a esse método é através da reflexão.
Daniel C. Sobral
Isso parece terrível. Especialmente: "Desconfie dos métodos de coleta - porque eles são polimórficos, a JVM não os otimiza. Você não precisa evitá-los, mas preste atenção nas seções críticas".
Matt
21

De acordo com o Benchmarks Game para um sistema de núcleo único e 32 bits, o Scala está em uma mediana 80% mais rápida que Java. O desempenho é aproximadamente o mesmo para um computador Quad Core x64. Até o uso da memória e a densidade do código são muito semelhantes na maioria dos casos. Eu diria, com base nessas análises (não científicas), que você está correto ao afirmar que o Scala adiciona alguma sobrecarga ao Java. Parece não adicionar toneladas de sobrecarga, então eu suspeitaria que o diagnóstico de itens de ordem superior ocupando mais espaço / tempo seja o mais correto.

Engenheiro Mundial
fonte
2
Para esta resposta por favor, use a comparação direta como a página de Ajuda sugere ( shootout.alioth.debian.org/help.php#comparetwo )
igouy
18
  • O desempenho do Scala é muito decente se você apenas escrever código Java / C no Scala. O compilador irá utilizar primitivas JVM para Int, Char, etc. quando pode. Enquanto loops são igualmente eficientes no Scala.
  • Lembre-se de que expressões lambda são compiladas em instâncias de subclasses anônimas das Functionclasses. Se você passar um lambda para map, a classe anônima precisará ser instanciada (e alguns locais podem precisar ser passados) e, em seguida, toda iteração terá sobrecarga adicional de chamada de função (com alguma passagem de parâmetro) das applychamadas.
  • Muitas classes scala.util.Randomsão apenas wrappers em torno de classes JRE equivalentes. A chamada de função extra é um pouco inútil.
  • Cuidado com implícitos no código crítico de desempenho. java.lang.Math.signum(x)é muito mais direto do que x.signum(), que converte para RichInte para trás.
  • O principal benefício de desempenho do Scala sobre Java é a especialização. Lembre-se de que a especialização é usada com moderação no código da biblioteca.
Daniel Lubarov
fonte
5
  • a) Pelo meu conhecimento limitado, devo observar, que o código no método principal estático não pode ser otimizado muito bem. Você deve mover o código crítico para um local diferente.
  • b) A partir de longas observações, eu recomendaria não fazer uma saída pesada no teste de desempenho (exceto que é exatamente o que você gostaria de otimizar, mas quem deve ler 2 milhões de valores?). Você está medindo println, o que não é muito interessante. Substituindo o println por max:
(1 to 20000).toList.map (_ * 2).max

reduz o tempo de 800 ms para 20 no meu sistema.

  • c) A compreensão é conhecida por ser um pouco lenta (embora tenhamos que admitir que está ficando melhor o tempo todo). Use as funções while ou recursivas. Não neste exemplo, onde está o loop externo. Use a anotação @ tailrec, para testar a capacidade de tairecursividade.
  • d) A comparação com C / Assembler falha. Você não reescreve o código scala para diferentes arquiteturas, por exemplo. Outra diferença importante para situações históricas é
    • Compilador JIT, otimizando em tempo real, e talvez dinamicamente, dependendo dos dados de entrada
    • A importância das falhas de cache
    • A crescente importância da invocação paralela. Hoje, a Scala tem soluções para trabalhar sem muita sobrecarga em paralelo. Isso não é possível em Java, exceto que você faz muito mais trabalho.
Usuário desconhecido
fonte
2
Eu removi o println do loop e, na verdade, o código Scala é mais rápido que o código Java.
Giorgio
A comparação com C e Assembler foi feita no seguinte sentido: uma linguagem de nível superior possui abstrações mais poderosas, mas pode ser necessário usar a linguagem de nível inferior para obter desempenho. Esse paralelo se mantém considerando Scala como o nível superior e Java como a linguagem de nível inferior? Talvez não, pois o Scala parece oferecer desempenho semelhante ao Java.
Giorgio
Eu não acho que isso importaria muito para Clojure ou Scala, mas quando eu costumava brincar com jRuby e Jython, provavelmente teria escrito o código mais crítico de desempenho em Java. Com esses dois, vi uma disparidade significativa, mas isso foi há anos atrás ... poderia ser melhor.
Rig