Comment optimiser pour-compréhensions et boucles en Scala?

donc Scala est supposé être aussi rapide que Java. Je revisite certains projet Euler problèmes en Scala que j'ai abordé à L'origine 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 0,7 secondes à compléter 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 directe" en Scala, qui prend 103 secondes (147 fois 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 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))
}

utilisant Scala 2.9.0.1 sous Windows 7 64-bit. 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?

129
demandé sur Peter Mortensen 2011-05-27 03:18:02

8 réponses

le problème dans ce cas particulier est que vous revenez de l'intérieur de la for-expression. Ce qui à son tour se traduit par un jet D'un NonLocalReturnException, qui est pris à la méthode enclosing. L'optimiseur peut éliminer l'avant-bras mais ne peut pas encore éliminer le lancer / la prise. Et de lancer/attraper est cher. Mais comme de tels retours imbriqués sont rares dans les programmes Scala, l'optimiseur n'a pas encore abordé ce cas. Il y a des travaux en cours pour améliorer l'optimiseur qui, espérons-le résoudre ce problème bientôt.

109
répondu Martin Odersky 2013-08-07 14:54:34

le problème est très probablement l'utilisation d'une for compréhension dans la méthode isEvenlyDivisible . Remplacer for par une boucle équivalente while devrait éliminer la différence de performance avec Java.

par opposition aux boucles for de Java, les compréhensions for de Scala sont en fait du sucre syntaxique pour les méthodes d'ordre supérieur; dans ce cas, vous appelez la méthode foreach sur un objet Range . Le for de Scala est très général, mais conduit parfois à des performances douloureuses.

vous pouvez essayer le drapeau -optimize dans la version 2.9 de Scala. Les performances observées peuvent dépendre de la JVM utilisée et de l'optimiseur JIT qui dispose d'un temps de "réchauffement" suffisant pour identifier et optimiser les points chauds.

des discussions récentes sur la liste de diffusion indiquent que L'équipe de Scala travaille à améliorer for la performance dans les cas simples:

voici le problème dans le bug tracker: https://issues.scala-lang.org/browse/SI-4633

mise à Jour 5/28 :

  • comme solution à court terme, le ScalaCL plugin (alpha) transformera les simples boucles Scala en l'équivalent des boucles while .
  • comme solution potentielle à long terme, les équipes de L'EPFL et de Stanford sont collaborer sur un projet permettant la compilation de temps d'exécution de "virtuel" Scala pour très haute performance. Par exemple, plusieurs idiomatiques les boucles fonctionnelles peuvent être fusionnées à l'exécution dans le bytecode optimal JVM, ou à une autre cible telle qu'un GPU. Le système est extensible, permettant DSLs défini par l'utilisateur et les transformations. Consultez les publications et Stanford notes de cours . Le code préliminaire est disponible sur Github, avec une sortie prévue dans les mois à venir.
80
répondu Kipton Barros 2011-05-28 17:33:43

comme suivi, j'ai essayé le drapeau-optimize et il a réduit le temps de fonctionnement de 103 à 76 secondes, mais c'est toujours 107x plus lent que Java ou une boucle while.

alors je regardais la version "fonctionnelle":

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 échoué lamentablement 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))
}

par lequel ma solution astucieuse de 5 lignes a diminué à 12 lignes. Cependant , cette version fonctionne en 0.71 secondes , la même vitesse que la version Java originale, et 56 fois plus rapide que la version ci-dessus en utilisant "forall" (40.2 s)! (voir éditer ci-dessous pour savoir pourquoi C'est plus rapide que Java)

évidemment, ma prochaine étape a été de traduire ce qui précède en Java, mais Java ne peut pas le gérer et lance un StackOverflowError avec n autour de la marque 22000.

j'ai alors griffé ma tête pendant un peu et 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))
}

donc la recursion de la queue de Scala gagne la journée, mais je suis surpris que quelque chose d'aussi simple qu'une boucle "for" (et la méthode "forall") soit essentiellement cassé et doit être remplacé par inélégant et verbeux "whiles", ou recursion de la queue. Une grande partie de la raison pour laquelle J'essaie Scala est en raison de la syntaxe concise, mais il est pas bon si mon le code va courir 100 fois plus lentement!

modifier : (supprimé)

EDIT OF EDIT : les anciennes différences entre les temps d'exécution de 2,5 s et 0,7 s étaient entièrement dues au fait que les JVM 32 bits ou 64 bits étaient utilisées. Scala de la ligne de commande utilise tout ce qui est défini par JAVA_HOME, tandis que Java utilise 64-bit si disponible indépendamment. Les IDEs ont leurs propres réglages. Quelques mesures ici: Scala temps d'exécution dans Eclipse

31
répondu Luigi Plinge 2017-05-23 12:26:00

La réponse à propos de la compréhension est bonne, mais elle n'est pas toute l'histoire. Vous devez noter que l'utilisation de return dans isEvenlyDivisible n'est pas gratuite. L'utilisation du retour à l'intérieur du for , force le compilateur scala à générer un retour non local (c'est-à-dire à retourner en dehors de sa fonction).

Ceci est fait par l'utilisation d'une exception pour sortir de la boucle. La même chose se produit si vous construisez vos propres 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 gravure "Salut" une seule fois.

notez que le return dans foo sort foo (ce qui est ce que vous attendez). Puisque l'expression entre crochets est une fonction littérale, que vous pouvez voir dans la signature de loop cela force le compilateur à générer un retour non local, c'est-à-dire, le return vous force à sortir foo , pas seulement le body .

en Java (C'est-à-dire la JVM) le seul façon de mettre en œuvre un tel comportement est à jeter une exception.

retour à isEvenlyDivisible :

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

Le 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.

8
répondu juancn 2011-06-16 13:02:11

quelques façons d'accélérer la méthode forall que j'ai découverte:

the original: 41.3 s

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

pré-instanciation de la gamme, donc nous ne créons pas une nouvelle gamme à chaque fois: 9.0 s

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

conversion à une liste au lieu d'une plage: 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 était le plus rapide (bien que toujours 7x plus lent que si nous évitons la portée et la fonction d'ordre supérieur tout à fait).

alors que je suis nouveau à Scala, je suppose que le compilateur pourrait facilement mettre en œuvre un gain de performance rapide et significative en remplaçant simplement automatiquement les littérales de gamme dans les méthodes (comme ci-dessus) avec des constantes de gamme dans la portée externe. Ou mieux, les interner comme des cordes littérales en Java.


note de bas de page : Les tableaux étaient à peu près les mêmes que la portée, mais il est intéressant de noter que la mise en place d'une nouvelle méthode forall (voir ci-dessous) a permis une exécution 24% plus rapide sur 64 bits, et 8% plus rapide sur 32 bits. Quand j'ai réduit la taille du calcul en réduisant le nombre de facteurs de 20 à 15 la différence a disparu, donc peut-être que c'est un effet de collecte des ordures. Quelle qu'en soit la cause, c'est important quand on fonctionne à pleine charge pendant de longues périodes.

un proxénète similaire pour List a également résulté dans environ 10% de meilleure performance.

  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)  
6
répondu Luigi Plinge 2011-06-19 06:01:39

je voulais juste commenter pour les gens qui pourraient perdre la foi en Scala sur des questions comme celle-ci que ce genre de questions surgissent dans la performance d'à peu près toutes les langues fonctionnelles. Si vous optimisez un pli dans Haskell, vous devrez souvent le réécrire sous forme de boucle recursive optimisée, sinon vous aurez des problèmes de performance et de mémoire à résoudre.

je sais qu'il est regrettable que les FPs ne soient pas encore optimisés 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.

3
répondu Ara Vartanian 2011-07-05 18:07:18

les problèmes spécifiques à Scala ont déjà été discutés, mais le problème principal est que l'utilisation d'un algorithme de force brute n'est pas très cool. Considérez ceci (beaucoup plus rapide que le code Java original):

def gcd(a: Int, b: Int): Int = {
    if (a == 0)
        b
    else
        gcd(b % a, a)
}
print (1 to 20 reduce ((a, b) => {
  a / gcd(a, b) * b
}))
2
répondu Sarge Borsch 2013-05-28 18:32:06

essayer la doublure unique donnée dans la solution Scala pour le projet Euler

le temps donné est au moins plus rapide que le vôtre, bien que loin de la boucle tout.. :)

1
répondu eivindw 2013-05-28 18:30:46