Desempenho do Scala comparado ao Java

39

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

Eu uso o Java há muito tempo e pretendo continuar usando-o. Paralelo a isso, atualmente estou aprendendo Scala com grande interesse: além das pequenas coisas que me levam a me acostumar com a minha impressão, posso realmente trabalhar muito bem nessa linguagem.

A 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 mais alto, como correspondência de padrões, funções de ordem superior, etc, introduzam alguma sobrecarga.

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

Se o Scala tiver alguma sobrecarga de Java, faz sentido ter projetos Scala / Java mistos, onde um codifica as partes mais complexas do Scala e as partes críticas de desempenho em Java? Isso é uma prática comum?

EDIT 1

Eu executei um pequeno benchmark: construa uma lista de inteiros, multiplique cada 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 corri tanto 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 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))
}

Então, 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 por Engenheiro Mundial.

Talvez existam outras construções do Scala que devam ser usadas com cuidado, porque elas são particularmente pesadas para serem executadas?

EDIT 2

Alguns de vocês apontaram que os printlns nos loops internos ocupam a maior parte o tempo de execução. Eu os removi e defini o tamanho das listas para 100.000 em vez de 20000. A média resultante foi 88 ms para Java e 49 ms para Scala.

    
por Giorgio 24.01.2012 / 19:47
fonte

5 respostas

37

Há uma coisa que você pode fazer de forma concisa e eficiente em Java que não é possível 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.

Então, na maior parte, você não precisa adicionar Java ao seu código. Mesmo para o código que usa enumeração em Java, muitas vezes há uma solução no Scala adequada ou boa - coloco a exceção em enumerações que possuem métodos extras e cujos valores constantes int 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)
    
  • Desconfie de 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 em seções críticas. Esteja ciente de que for no Scala é implementado por meio de chamadas de método e classes anônimas.

  • Se você estiver usando uma classe Java, como String , Array ou AnyVal classes que correspondem às primitivas Java, prefira os métodos fornecidos pelo Java quando existirem alternativas. Por exemplo, use length on String e Array em vez de size .

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

  • Estenda classes em vez de traços. Por exemplo, se você estiver estendendo Function1 , estenda AbstractFunction1 .

  • Use -optimise e especialização para obter a maior parte do Scala.

  • Entenda o que está acontecendo: javap é seu amigo e, por isso, há várias bandeiras do Scala que mostram o que está acontecendo.

  • As expressões Scala são projetadas para melhorar a correção e tornar o código mais conciso e sustentável. Eles não são projetados para velocidade, por isso, se você precisar usar null em vez de Option em um caminho crítico, faça isso! Há uma razão pela qual o Scala é multi-paradigma.

  • Lembre-se de que a verdadeira medida de desempenho é a execução do código. Consulte esta pergunta para ver um exemplo do que pode acontecer se você ignorar essa regra.

por 24.01.2012 / 23:04
fonte
20

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

    
por 24.01.2012 / 20:21
fonte
18
  • O desempenho do Scala é muito decente se você simplesmente escrever código semelhante ao Java / C no Scala. O compilador usará primitivas de JVM para Int , Char , etc. quando puder. Enquanto loops são tão eficientes em Scala.
  • Lembre-se de que expressões lambda são compiladas para instâncias de subclasses anônimas das classes Function . Se você passar um lambda para map , a classe anônima precisará ser instanciada (e alguns locais talvez precisem ser passados) e, em seguida, cada iteração terá sobrecarga de chamada de função extra (com a passagem de alguns parâmetros) das chamadas apply .
  • Muitas classes como scala.util.Random são apenas wrappers em torno de classes equivalentes do JRE. A chamada de função extra é um pouco perdulária.
  • Cuidado com os implícitos no código de desempenho crítico. java.lang.Math.signum(x) é muito mais direto que x.signum() , que converte em RichInt e de volta.
  • O principal benefício de desempenho do Scala em relação ao Java é a especialização. Tenha em mente que a especialização é usada com parcimônia no código da biblioteca.
por 24.01.2012 / 21:51
fonte
5
  • a) Do meu conhecimento limitado, devo observar que o código no método main 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 observações longas, eu recomendaria não fazer uma saída pesada no teste de desempenho (exceto é exatamente o que você gosta de otimizar, mas quem deve ler valores de 2 Milhões?). 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) O entendimento é conhecido por ser um pouco lento (enquanto temos que admitir que está melhorando o tempo todo). Use funções while ou tailrecursive. Não neste exemplo, onde é o loop externo. Use o @ tailrec-annotation, para testar a tairecursividade.
  • d) A comparação com o C / Assembler falha. Você não reescreve o código scala para arquiteturas diferentes, por exemplo. Outra diferença importante para situações históricas são
    • 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. A Scala hoje tem soluções para trabalhar sem muita sobrecarga em paralelo. Isso não é possível em Java, exceto que você trabalha muito mais.
por 24.01.2012 / 23:15
fonte
1

Confira meu post há mais um ano sobre o desempenho do Scala.

link

Eu fiz uma comparação entre uma construção de scala comum (usando Range) e uma implicação iterativa usando um contador de loop de tipos long e int. Tem alguns resultados interessantes. Também experimentei o plugin scalacl para otimizar várias construções funcionais.

Note que você pode observar que minha experiência NÃO está realmente testando o desempenho de scala, mas sim o desempenho de certas classes de biblioteca, como o Range.

Note também que os microbenchmarks são sempre um pouco complicados.

    
por 14.02.2013 / 00:54
fonte