Comment optimiser pour des inclusions et des boucles en Scala?
Scala est censé être aussi rapide que Java. Je suis de revoir certaines Projet Euler problèmes en Scala que j'ai d'abord abordé en Java. Plus précisément Problème 5: "Quel est le plus petit nombre positif qui est divisible par tous les nombres de 1 à 20?"
Voici ma solution Java, qui prend de 0,7 secondes sur ma machine:
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();
}
}
Voici ma "traduction" en Scala, qui prend 103 secondes (147 fois de plus!)
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
}
}
Enfin voici ma tentative de programmation fonctionnelle, qui prend en 39 secondes (55 fois plus longtemps)
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))
}
Utilisation de Scala 2.9.0.1 sur Windows 7 64 bits. Comment puis-je améliorer les performances? Suis-je en train de faire quelque chose de mal? Ou est Java il y a beaucoup plus vite?
- avez-vous la compilation ou de l'interpréter à l'aide de la scala de shell?
- Il y a une meilleure façon de le faire qu'à l'aide de la section de première instance (Indice).
- vous n'avez pas de montrer comment vous êtes timing de cette. Avez-vous essayez juste de chronométrage de la
run
méthode? - C'est compilé, pas de coquille.
- J'ai chronométré juste la méthode de course en utilisant le Système.nanotime() en Java. Un physique chronomètre de la Scala versions
- yep, juste fait la pen & version papier: écrire les facteurs premiers de chaque numéro du haut, puis traverser les facteurs que vous avez déjà pour les numéros les plus élevés, de sorte que vous avez terminé avec (5*2*2)*(19)*(3*3)*(17)*(2*2)*()*(7)*(13)*()*(11) = 232792560
- +1 C'est la question la plus intéressante que j'ai vu depuis des semaines sur SO (qui possède également la meilleure réponse que j'ai vu dans un certain temps).
- +1 pour dire "aussi vite que Java".
- balle - la mise en œuvre exécutable n'est pas la même chose que "la fraie un nouveau fil de discussion."
new Thread(new Runnable() { public void run() { ... } })
, cependant, est la même que la reproduction d'un nouveau thread. - J'ai l'habitude de mettre en œuvre Praticable dans mes classes Java qui sont destinés à être exécutés: il est plus conceptuel sens que statique "principal" de la méthode, et je peux facilement lancer dans un nouveau thread d'ailleurs (par exemple, un Swing GUI). Mais je devrais probablement avoir oublié pendant cette discussion parce que c'est sans importance lorsque nous utilisons un "principal".
Vous devez vous connecter pour publier un commentaire.
Le problème dans ce cas particulier, c'est que vous renvoyer à partir de l'intérieur de l'expression. Qui à son tour se traduit en une projection d'un NonLocalReturnException, qui est capturé à la enfermant méthode. L'optimiseur peut éliminer le foreach mais ne peut pas encore éliminer le lancer/attraper. Et de lancer/attraper est cher. Mais puisque de tels imbriqués les retours sont rares dans la Scala de programmes, l'optimiseur n'a pas encore répondre à cette affaire. Il y a des travaux en cours pour améliorer l'optimiseur qui nous l'espérons, permettra de résoudre ce problème bientôt.
Le problème est probablement l'utilisation d'un
for
la compréhension de la méthodeisEvenlyDivisible
. Remplacement defor
par un équivalentwhile
boucle d'éliminer la différence de performances avec Java.Contrairement à Java
for
boucles, Scalafor
compréhensions sont effectivement sucre syntaxique pour des modes de commande; dans ce cas, vous êtes à l'appel de laforeach
méthode sur unRange
objet. Scalafor
est très générale, mais entraîne parfois douloureux de la performance.Vous pourriez vouloir essayer le
-optimize
drapeau Scala version 2.9. Observé les performances peuvent varier en fonction de la JVM en cours d'utilisation, et le JIT optimiseur de disposer de suffisamment de temps "d'échauffement" pour identifier et optimiser les hot-spots.Récentes discussions sur la liste de diffusion indiquent que la Scala équipe travaille sur l'amélioration de
for
performances dans les cas simples:Ici est la question dans le bug tracker:
https://issues.scala-lang.org/browse/SI-4633
Mise à jour 5/28:
while
boucles.for
convenable alors?Comme un suivi, j'ai essayé de l'optimiser drapeau et il réduit le temps d'exécution à partir de 103 à 76 secondes, mais c'est encore 107x plus lent que Java ou une boucle while.
Alors que je regardais le "fonctionnel" version:
et à essayer de comprendre comment se débarrasser de la "forall" d'une manière concise. J'ai lamentablement échoué et est venu avec
lequel mon ruse 5-solution en ligne a balooned à 12 lignes. Cependant, cette version fonctionne en 0.71 secondes, la même vitesse que l'original de la version de Java, et 56 fois plus rapide que la version ci-dessus à l'aide de "forall" (40.2 s)! (voir modification ci-dessous pour savoir pourquoi cela est plus rapide que Java)
Évidemment, ma prochaine étape était de traduire le ci-dessus dans Java, mais Java ne peut pas le manipuler et jette un StackOverflowError avec n autour de l'22000 marque.
Ensuite, je grattais la tête un peu et a remplacé le "tout" avec un peu plus de la queue de la récursivité, qui enregistre un couple de lignes, fonctionne tout aussi rapide, mais avouons-le, est plus difficile à lire:
Scala, la queue de la récursivité remporte le jour, mais je suis surpris que quelque chose d'aussi simple que d'une boucle "for" (et le "forall" méthode) est essentiellement cassé et doit être remplacé par inélégant et verbose "laps de temps", ou de la queue de la récursivité. Beaucoup de la raison pour laquelle je suis en train de Scala est en raison de la concision de la syntaxe, mais c'est pas bon si mon code va s'exécuter 100 fois plus lent!
MODIFIER: (supprimé)
EDIT DE l'EDIT: Ancien écarts entre les temps d'exécution de 2,5 s et 0,7 s ont été entièrement attribuable à savoir si le 32-bits ou 64-bits les machines virtuelles ont été utilisés. Scala à partir de la ligne de commande utilise tout ce qui est défini par JAVA_HOME, alors que Java utilise 64 bits si disponible, peu importe. IDEs ont leurs propres paramètres. Certaines mesures ici: Scala temps d'exécution dans Eclipse
def isDivis(x: Int, i: Int): Boolean = if (i > 20) true else if (x % i != 0) false else isDivis(x, i+1)
. Remarquez que dans Scala if-else est une expression qui retourne toujours une valeur. Pas besoin pour le retour-mot-clé ici.P005_V3
) peut être raccourcie, plus déclaratif et à mon avis plus clair en écrivant:def isDivis(x: Int, i: Int): Boolean = (i > 20) || (x % i == 0) && isDivis(x, i+1)
La réponse à propos de la compréhension est bonne, mais elle n'est pas toute l'histoire. Vous devriez noter de noter que l'utilisation de
return
dansisEvenlyDivisible
n'est pas libre. L'utilisation de retourner à l'intérieur de lafor
, les forces de la scala compilateur pour générer un non-retour locale (c'est à dire de retour à l'extérieur de sa fonction).Ceci est fait grâce à l'utilisation d'une exception à la sortie de la boucle. La même chose se produit si vous construisez votre propre abstractions de contrôle, par exemple:
Cette affiche "Salut" qu'une seule fois.
Noter que le
return
dansfoo
sortiesfoo
(qui est ce que vous attendez). Depuis l'expression entre crochets est une fonction littérale, que vous pouvez voir dans la signature deloop
cela force le compilateur à générer un non retour, c'est lereturn
vous oblige à quitterfoo
, et pas seulement lesbody
.En Java (c'est à dire la JVM) la seule façon de mettre en œuvre un tel comportement est à jeter une exception.
Revenir à
isEvenlyDivisible
:La
if (a % i != 0) return false
est un littéral de fonction qui a un retour, de sorte que chaque fois que le retour est atteint, le moteur d'exécution est à lancer et à attraper une exception, ce qui provoque un peu de GC frais généraux.Certains des moyens d'accélérer le
forall
méthode que j'ai découvert:L'original: 41,3 s
Avant l'instanciation de la gamme, afin de ne pas créer une nouvelle gamme de tous les temps: 9.0 s
De conversion d'une Liste au lieu d'une Gamme: 4,8 s
J'ai essayé quelques autres collections, mais la Liste a été la plus rapide (bien que toujours 7x plus lent que si nous ne voulons pas de la Plage et de la fonction d'ordre supérieur au total).
Pendant que je suis à nouveau à la Scala, je suppose que le compilateur pourrait facilement mettre en œuvre rapide et un gain de performance par tout simplement de remplacer automatiquement la Gamme des littéraux dans les méthodes (comme ci-dessus) avec la Gamme des constantes dans le contexte le plus externe. Ou mieux, stagiaire comme des Chaînes de caractères littérales en Java.
note de bas de page:
Les tableaux ont environ la même que la Gamme, mais il est intéressant de noter, le proxénétisme, une nouvelle
forall
méthode (voir ci-dessous) a abouti à 24% plus rapide d'exécution sur 64 bits, et 8% plus rapide sur 32 bits. Quand j'ai réduit le calcul de la taille en réduisant le nombre de facteurs de 20 à 15 la différence a disparu, donc c'est peut-être une collecte des ordures effet. Quelle que soit la cause, il est significatif lors du fonctionnement à pleine charge pendant des périodes prolongées.Similaire pimp pour la Liste a également entraîné environ 10% de meilleures performances.
Je voulais juste commenter pour les personnes qui risquent de perdre la foi en Scala, sur des questions comme ce que ces sortes de questions viennent à l'exécution d'à peu près tous les langages fonctionnels. Si vous êtes à l'optimisation d'un pli en Haskell, vous aurez souvent à ré-écrire comme un appel récursif à la queue-appel-optimisé boucle, sinon vous aurez des performances et des problèmes de mémoire pour composer avec.
Je sais qu'il est malheureux que les FPs ne sont pas encore optimisées au point où nous n'avons pas à penser à des choses comme cela, mais ce n'est pas du tout un problème particulier à la Scala.
Problèmes spécifiques à la Scala ont déjà été discutés, mais le principal problème est que l'utilisation d'une force brute de l'algorithme n'est pas très cool. Considérez ceci (beaucoup plus rapide que l'original du code Java):
Essayer le one-liner donnée dans la solution Scala pour Projet d'Euler
Le temps donné est au moins plus vite que le vôtre, bien que loin de la boucle while.. 🙂
def r(n:Int):Int = if ((1 to 20) forall {n % _ == 0}) n else r (n+2); r(2)
, qui est de 4 caractères plus court que Pavel. 🙂 Je ne prétends pas que mon code est tout bon - lorsque j'ai posté cette question que j'avais codé un total d'environ 30 lignes de la Scala.