131 votes

Comment optimiser des compréhensions et des boucles en Scala ?

Scala est censé être aussi rapide que Java. Je suis de revoir certaines de 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 de temps)

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?

111voto

Martin Odersky Points 13161

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.

80voto

Kipton Barros Points 12445

Le problème est probablement l'utilisation d'un for de la compréhension de la méthode isEvenlyDivisible. Remplacement d' for par un équivalent while boucle d'éliminer la différence de performances avec Java.

Contrairement à Java for boucles, Scala for compréhensions sont effectivement sucre syntaxique pour des modes de commande; dans ce cas, vous êtes à l'appel de la foreach méthode de Range objet. Scala for est très générale, mais entraîne parfois douloureux de la performance.

Vous voudrez peut-être essayer l' -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.

Les récentes discussions sur la liste de diffusion indiquent que la Scala équipe est de travailler sur l'amélioration de l' for de la performance dans les cas simples:

Voici la question dans le bug tracker: https://issues.scala-lang.org/browse/SI-4633

Mise à jour 5/28:

  • Comme une solution à court terme, la ScalaCL plugin (alpha) va transformer de simples Scala boucles en l'équivalent de l' while boucles.
  • Comme une éventuelle solution à plus long terme, les équipes de l'EPFL et de Stanford sont collaborer sur un projet permettant d'exécution de la compilation de "virtuel" Scala de très haute performance. Par exemple, plusieurs idiomatiques fonctionnels boucles peuvent être fusionnés au moment de l'exécution optimale dans le bytecode JVM, ou à une autre cible, telle qu'un GPU. Le système est extensible, permettant à l'utilisateur défini Dsl et les transformations. Découvrez les publications et Stanford notes de cours. Préliminaire du code est disponible sur Github, avec une sortie prévue dans les mois à venir.

31voto

Luigi Plinge Points 23552

Comme suite, 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.

Ensuite, j'ai été à la recherche à la "fonctionnelle" de la version:

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))
}

et à essayer de comprendre comment se débarrasser de la "forall" d'une manière concise. J'ai lamentablement échoué et est venu avec

object P005_V2 extends App {
  def isDivis(x:Int):Boolean = {
    var i = 1
    while(i <= 20) {
      if (x % i != 0) return false
      i += 1
    }
    return true
  }
  def find(n:Int):Int = if (isDivis(n)) n else find (n+2)
  println (find (2))
}

lequel mon ruse 5-solution en ligne a balooned à 12 lignes. Cependant, cette version fonctionne dans 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:

object P005_V3 extends App {
  def isDivis(x:Int, i:Int):Boolean = 
    if(i > 20) true
    else if(x % i != 0) false
    else isDivis(x, i+1)

  def find(n:Int):Int = if (isDivis(n, 2)) n else find (n+2)
  println (find (2))
}

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!

EDIT: (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

8voto

juancn Points 1077

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 l' return en isEvenlyDivisible n'est pas libre. L'utilisation de retourner à l'intérieur de l' for, 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:

def loop[T](times: Int, default: T)(body: ()=>T) : T = {
    var count = 0
    var result: T = default
    while(count < times) {
        result = body()
        count += 1
    }
    result
}

def foo() : Int= {
    loop(5, 0) {
        println("Hi")
        return 5
    }
}

foo()

Cette affiche "Salut" qu'une seule fois.

Notez que l' return en foo sorties foo (qui est ce que vous attendez). Depuis l'expression entre crochets est une fonction littérale, que vous pouvez voir dans la signature de l' loop cela force le compilateur à générer un non retour, c'est l' return vous oblige à quitter foo, et pas seulement l' body.

En Java (c'est à dire la JVM) la seule façon de mettre en œuvre un tel comportement est à jeter une exception.

Remontant à l' isEvenlyDivisible:

def isEvenlyDivisible(a:Int, b:Int):Boolean = {
  for (i <- 2 to b) 
    if (a % i != 0) return false
  return true
}

L' 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.

6voto

Luigi Plinge Points 23552

Certains des moyens d'accélérer l' forall méthode que j'ai découvert:

L'original: 41,3 s

def isDivis(x:Int) = (1 to 20) forall {x % _ == 0}

Avant l'instanciation de la gamme, afin de ne pas créer une nouvelle gamme de tous les temps: 9.0 s

val r = (1 to 20)
def isDivis(x:Int) = r forall {x % _ == 0}

La conversion d'une Liste au lieu d'une Gamme: 4,8 s

val rl = (1 to 20).toList
def isDivis(x:Int) = rl forall {x % _ == 0}

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 un nouveau 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.

Une semblable pimp pour la Liste a également entraîné environ 10% de meilleures performances.

  val ra = (1 to 20).toArray
  def isDivis(x:Int) = ra forall2 {x % _ == 0}

  case class PimpedSeq[A](s: IndexedSeq[A]) {
    def forall2 (p: A => Boolean): Boolean = {      
      var i = 0
      while (i < s.length) {
        if (!p(s(i))) return false
        i += 1
      }
      true
    }    
  }  
  implicit def arrayToPimpedSeq[A](in: Array[A]): PimpedSeq[A] = PimpedSeq(in)  

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X