Portanto, Scala deve ser tão rápido quanto Java. Estou revisitando alguns problemas do Project Euler no Scala que originalmente lidei em Java. Especificamente Problema 5: "Qual é o menor número positivo que é igualmente divisível por todos os números de 1 a 20?"
Aqui está minha solução Java, que leva 0,7 segundos para ser concluída na minha máquina:
public class P005_evenly_divisible implements Runnable{
final int t = 20;
public void run() {
int i = 10;
while(!isEvenlyDivisible(i, t)){
i += 2;
}
System.out.println(i);
}
boolean isEvenlyDivisible(int a, int b){
for (int i = 2; i <= b; i++) {
if (a % i != 0)
return false;
}
return true;
}
public static void main(String[] args) {
new P005_evenly_divisible().run();
}
}
Aqui está minha "tradução direta" para Scala, que leva 103 segundos (147 vezes mais!)
object P005_JavaStyle {
val t:Int = 20;
def run {
var i = 10
while(!isEvenlyDivisible(i,t))
i += 2
println(i)
}
def isEvenlyDivisible(a:Int, b:Int):Boolean = {
for (i <- 2 to b)
if (a % i != 0)
return false
return true
}
def main(args : Array[String]) {
run
}
}
Finalmente, aqui está minha tentativa de programação funcional, que leva 39 segundos (55 vezes mais)
object P005 extends App{
def isDivis(x:Int) = (1 to 20) forall {x % _ == 0}
def find(n:Int):Int = if (isDivis(n)) n else find (n+2)
println (find (2))
}
Usando o Scala 2.9.0.1 no Windows 7 de 64 bits. Como melhoro o desempenho? Estou fazendo algo errado? Ou o Java é muito mais rápido?
java
performance
scala
for-loop
while-loop
Luigi Plinge
fonte
fonte
run
método?Respostas:
O problema nesse caso específico é que você retorna de dentro da expressão. Por sua vez, isso é traduzido em um lançamento de NonLocalReturnException, que é capturado no método anexo. O otimizador pode eliminar o foreach, mas ainda não pode eliminar o lançamento / captura. E jogar / pegar é caro. Mas como esses retornos aninhados são raros nos programas Scala, o otimizador ainda não abordou esse caso. Há trabalho em andamento para melhorar o otimizador que, esperamos, resolva esse problema em breve.
fonte
O problema provavelmente é o uso de uma
for
compreensão no métodoisEvenlyDivisible
. A substituiçãofor
por umwhile
loop equivalente deve eliminar a diferença de desempenho com Java.Ao contrário dos
for
loops de Java , asfor
compreensões do Scala são na verdade açúcar sintático para métodos de ordem superior; Nesse caso, você está chamando oforeach
método em umRange
objeto. O Scalafor
é muito geral, mas às vezes leva a um desempenho doloroso.Você pode tentar a
-optimize
bandeira no Scala versão 2.9. O desempenho observado pode depender da JVM específica em uso e do otimizador de JIT com tempo de "aquecimento" suficiente para identificar e otimizar pontos de acesso.Discussões recentes na lista de discussão indicam que a equipe Scala está trabalhando para melhorar o
for
desempenho em casos simples:Aqui está o problema no rastreador de erros: https://issues.scala-lang.org/browse/SI-4633
Atualização 5/28 :
while
loops.fonte
for
adequado então?Como acompanhamento, tentei o sinalizador -optimize e ele reduziu o tempo de execução de 103 para 76 segundos, mas ainda é 107x mais lento que o Java ou um loop while.
Então eu estava olhando para a versão "funcional":
e tentando descobrir como se livrar do "forall" de maneira concisa. Eu falhei miseravelmente e inventei
em que minha solução astuta de 5 linhas aumentou para 12 linhas. No entanto, esta versão é executada em 0,71 segundos , a mesma velocidade que a versão Java original e 56 vezes mais rápida que a versão acima usando "forall" (40,2 s)! (veja EDITAR abaixo para saber por que isso é mais rápido que Java)
Obviamente, meu próximo passo foi converter o código acima em Java, mas o Java não pode lidar com isso e lança um StackOverflowError com n em torno da marca 22000.
Em seguida, coçou a cabeça um pouco e substituí o "while" por um pouco mais de recursão na cauda, o que economiza algumas linhas, corre tão rápido quanto, mas, convenhamos, é mais confuso ler:
Portanto, a recursão da cauda de Scala vence o dia, mas estou surpreso que algo tão simples quanto um loop "for" (e o método "forall") esteja essencialmente quebrado e precise ser substituído por "whiles" deselegantes e detalhados ou recursão da cauda . Muitas das razões pelas quais estou experimentando o Scala são por causa da sintaxe concisa, mas não é bom se meu código for executado 100 vezes mais lento!
EDITAR : (excluído)
EDIT OF EDIT : As discrepâncias anteriores entre os tempos de execução de 2,5s e 0,7s eram inteiramente devidas ao fato de as JVMs de 32 ou 64 bits estarem sendo usadas. O Scala na linha de comando usa o que é definido por JAVA_HOME, enquanto o Java usa 64 bits, se disponível, independentemente. Os IDEs têm suas próprias configurações. Algumas medidas aqui: Tempos de execução do Scala no Eclipse
fonte
def isDivis(x: Int, i: Int): Boolean = if (i > 20) true else if (x % i != 0) false else isDivis(x, i+1)
. Observe que em Scala if-else é uma expressão que sempre retorna um valor. Não há necessidade da palavra-chave de retorno aqui.P005_V3
) pode ser mais curta, mais declarativa e mais clara no IMHO, escrevendo:def isDivis(x: Int, i: Int): Boolean = (i > 20) || (x % i == 0) && isDivis(x, i+1)
A resposta sobre a compreensão está certa, mas não é a história toda. Você deve observar que o uso de
return
inisEvenlyDivisible
não é gratuito. O uso de return dentro dofor
, força o compilador scala a gerar um retorno não local (ou seja, retornar fora de sua função).Isso é feito através do uso de uma exceção para sair do loop. O mesmo acontece se você criar suas próprias abstrações de controle, por exemplo:
Isso imprime "Oi" apenas uma vez.
Note que o
return
emfoo
saídasfoo
(que é o que você esperaria). Como a expressão entre colchetes é uma função literal, que você pode ver na assinaturaloop
disso força o compilador a gerar um retorno não local, ou seja, asreturn
forças que você deve sairfoo
, não apenas obody
.Em Java (ou seja, a JVM), a única maneira de implementar esse comportamento é lançar uma exceção.
Voltando a
isEvenlyDivisible
:A
if (a % i != 0) return false
é uma literal de função que tem um retorno; portanto, toda vez que o retorno é atingido, o tempo de execução deve gerar e capturar uma exceção, o que causa um pouco de sobrecarga do GC.fonte
Algumas maneiras de acelerar o
forall
método que eu descobri:O original: 41.3 s
Pré-instanciando o intervalo, para não criarmos um novo intervalo sempre: 9,0 s
Convertendo para uma lista em vez de um intervalo: 4.8 s
Tentei algumas outras coleções, mas a Lista foi mais rápida (embora ainda 7 vezes mais lenta do que se evitássemos a função Faixa e ordem superior).
Embora eu seja novo no Scala, acho que o compilador pode implementar facilmente um ganho de desempenho rápido e significativo, simplesmente substituindo automaticamente literais Range nos métodos (como acima) por constantes Range no escopo mais externo. Ou melhor, interne-os como strings literais em Java.
nota de rodapé : as matrizes eram quase as mesmas que Range, mas, curiosamente, criando um novo
forall
método (mostrado abaixo) resultou em uma execução 24% mais rápida em 64 bits e 8% mais rápida em 32 bits. Quando reduzi o tamanho do cálculo, reduzindo o número de fatores de 20 para 15, a diferença desapareceu, talvez seja um efeito de coleta de lixo. Qualquer que seja a causa, é significativo ao operar sob carga máxima por longos períodos.Um proxeneta semelhante para a List também resultou em um desempenho 10% melhor.
fonte
Eu só queria comentar para as pessoas que podem perder a fé no Scala sobre questões como esta que esses tipos de problemas surgem no desempenho de praticamente todas as linguagens funcionais. Se você estiver otimizando uma dobra no Haskell, geralmente precisará reescrevê-la como um loop otimizado de chamada de cauda recursiva; caso contrário, terá problemas de desempenho e memória para lidar.
Eu sei que é lamentável que os FPs ainda não estejam otimizados a ponto de não precisarmos pensar em coisas como essa, mas esse não é um problema específico da Scala.
fonte
Problemas específicos do Scala já foram discutidos, mas o principal problema é que o uso de um algoritmo de força bruta não é muito legal. Considere isso (muito mais rápido que o código Java original):
fonte
Experimente o one-liner fornecido na solução Scala for Project Euler
O tempo indicado é pelo menos mais rápido que o seu, embora longe do loop while .. :)
fonte
def r(n:Int):Int = if ((1 to 20) forall {n % _ == 0}) n else r (n+2); r(2)
, que é 4 caracteres menor que o de Pavel. :) No entanto, não pretendo que meu código seja bom - quando postei essa pergunta, codifiquei um total de cerca de 30 linhas de Scala.