Performances de Scala par rapport à Java

41

Tout d’abord, je voudrais préciser que ce n’est pas une question langue X-langue-Y qui permet de déterminer laquelle est la meilleure.

J'utilise Java depuis longtemps et j'ai l'intention de continuer à l'utiliser. Parallèlement à cela, j'apprends actuellement Scala avec un grand intérêt: mis à part quelques petites choses qui prennent un certain temps pour s'habituer à mon impression, c'est que je peux vraiment très bien travailler dans cette langue.

Ma question est la suivante: comment les logiciels écrits en Scala se comparent-ils aux logiciels écrits en Java en termes de vitesse d'exécution et de consommation de mémoire? Bien sûr, il est difficile de répondre à cette question en général, mais je m'attendrais à ce que les constructions de niveau supérieur telles que la correspondance de modèle, les fonctions d'ordre supérieur, etc. introduisent une surcharge.

Cependant, mon expérience actuelle à Scala se limite à de petits exemples de moins de 50 lignes de code et je n'ai pas encore effectué de tests de performances. Donc, je n'ai pas de données réelles.

S’il s’avère que Scala a une surcharge en Java, est-il judicieux d’avoir des projets Scala / Java mixtes, dans lesquels on code les parties les plus complexes de Scala et les parties critiques en termes de performance en Java? Est-ce une pratique courante?

EDIT 1

J'ai exécuté un petit test d'évaluation: créez une liste d'entiers, multipliez chaque entier par deux et mettez-la dans une nouvelle liste, imprimez la liste résultante. J'ai écrit une implémentation Java (Java 6) et une implémentation Scala (Scala 2.9). J'ai couru les deux sur Eclipse Indigo sous Ubuntu 10.04.

Les résultats sont comparables: 480 ms pour Java et 493 ms pour Scala (moyenne sur 100 itérations). Voici les extraits que j'ai utilisés.

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

Ainsi, dans ce cas, il semble que les frais généraux de Scala (utilisant range, map, lambda) soient vraiment minimes, ce qui n’est pas loin des informations fournies par World Engineer.

Peut-être y a-t-il d'autres constructions Scala qui devraient être utilisées avec précaution car elles sont particulièrement lourdes à exécuter?

EDIT 2

Certains d'entre vous ont fait remarquer que les impressions dans les boucles internes occupent la majeure partie du temps d'exécution. Je les ai supprimés et ai défini la taille des listes sur 100000 au lieu de 20000. La moyenne résultante était de 88 ms pour Java et de 49 ms pour Scala.

Giorgio
la source
5
J'imagine que depuis que Scala compile en code octet JVM, les performances pourraient théoriquement être équivalentes à celles de Java s'exécutant sous la même JVM, toutes choses étant égales par ailleurs. La différence, je pense, réside dans la manière dont le compilateur Scala crée le code octet et s’il le fait de manière efficace.
maple_shaft
2
@maple_shaft: Ou peut-être y a-t-il une surcharge dans le temps de compilation de Scala?
FrustratedWithFormsDesigner
1
@Giorgio Il n'y a pas de distinction au moment de l'exécution entre les objets Scala et les objets Java, ce sont tous des objets JVM qui sont définis et se comportent selon le code d'octet. Scala, par exemple, en tant que langage a le concept de fermeture, mais lorsque celles-ci sont compilées, elles sont compilées en un certain nombre de classes avec du code octet. Théoriquement, je pourrais écrire physiquement du code Java compilant exactement le même code octet et dont le comportement à l’exécution serait exactement le même.
maple_shaft
2
@maple_shaft: C'est exactement ce que je souhaite: je trouve le code Scala ci-dessus beaucoup plus concis et lisible que le code Java correspondant. Je me demandais simplement s'il était logique d'écrire des parties d'un projet Scala en Java pour des raisons de performances et quelles seraient ces parties.
Giorgio
2
Le runtime sera en grande partie occupé par les appels println. Vous avez besoin d'un test plus intensif en calcul.
kevin cline

Réponses:

39

Il y a une chose que vous pouvez faire avec concision et efficacité en Java et que vous ne pouvez pas utiliser dans Scala: les énumérations. Pour tout le reste, même pour les constructions lentes dans la bibliothèque de Scala, vous pouvez obtenir des versions efficaces fonctionnant dans Scala.

Donc, pour la plupart, vous n'avez pas besoin d'ajouter Java à votre code. Même pour le code qui utilise l'énumération en Java, il existe souvent une solution adéquate ou bonne dans Scala: je place l'exception sur les énumérations qui ont des méthodes supplémentaires et dont les valeurs de constante int sont utilisées.

En ce qui concerne ce qu'il faut surveiller, voici certaines choses.

  • Si vous utilisez le modèle enrichir ma bibliothèque, convertissez-le toujours en classe. Par exemple:

    // 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)
  • Méfiez-vous des méthodes de collecte - car elles sont généralement polymorphes, JVM ne les optimise pas. Vous n'avez pas besoin de les éviter, mais faites attention aux sections critiques. Sachez que forScala est implémenté via des appels de méthodes et des classes anonymes.

  • Si vous utilisez une classe Java, telle que String, Arrayou des AnyValclasses correspondant à des primitives Java, préférez les méthodes fournies par Java lorsque des alternatives existent. Par exemple, utilisez lengthon Stringet à la Arrayplace de size.

  • Évitez d’utiliser imprudemment les conversions implicites, car vous pouvez vous retrouver à utiliser des conversions par erreur plutôt que de par leur conception.

  • Étendre les classes au lieu des traits. Par exemple, si vous étendez Function1, étendez à la AbstractFunction1place.

  • Utilisation -optimiseet spécialisation pour tirer le meilleur parti de Scala.

  • Comprenez ce qui se passe: javapvotre ami est-il, ainsi que de nombreux drapeaux Scala indiquant ce qui se passe.

  • Les idiomes Scala sont conçus pour améliorer la correction et rendre le code plus concis et facile à gérer. Ils ne sont pas conçus pour la vitesse, donc si vous devez utiliser nullplutôt que Optionsur un chemin critique, faites-le! Il y a une raison pour laquelle Scala est multi-paradigme.

  • N'oubliez pas que la véritable mesure des performances consiste à exécuter du code. Voir cette question pour un exemple de ce qui peut arriver si vous ignorez cette règle.

Daniel C. Sobral
la source
1
+1: Beaucoup d'informations utiles, même sur des sujets qu'il me reste à apprendre, mais il est utile d'avoir lu quelques indices avant de pouvoir les consulter.
Giorgio
Pourquoi la première approche utilise la réflexion? De toute façon, il génère une classe anonyme, alors pourquoi ne pas l'utiliser au lieu de réflexion?
Oleksandr.Bezhan
@ Oleksandr.Bezhan Anonymous class est un concept Java, pas un concept Scala. Il génère un raffinement de type. Une méthode de classe anonyme qui ne remplace pas sa classe de base est inaccessible de l'extérieur. Il n'en va pas de même pour les raffinements de types de Scala. Le seul moyen de parvenir à cette méthode est donc la réflexion.
Daniel C. Sobral
Cela semble assez terrible. En particulier: "Méfiez-vous des méthodes de collecte - car elles sont polymorphes pour la plupart, JVM ne les optimise pas. Vous ne devez pas les éviter, mais faites attention aux sections critiques."
Mat
21

Selon le jeu de référence pour un système 32 bits à cœur unique, Scala se situe à une médiane de 80% aussi rapide que Java. Les performances sont approximativement les mêmes pour un ordinateur Quad Core x64. Même l' utilisation de la mémoire et la densité de code sont très similaires dans la plupart des cas. Je dirais, sur la base de ces analyses (plutôt non scientifiques), que vous avez raison d'affirmer que Scala ajoute une surcharge à Java. Cela ne semble pas ajouter des tonnes de frais généraux, donc je suppose que le diagnostic du plus grand nombre d'articles nécessitant une plus grande quantité d'espace / de temps est le plus correct.

Ingénieur du monde
la source
2
Pour cette réponse, utilisez simplement la comparaison directe suggérée par la page d'aide ( shootout.alioth.debian.org/help.php#comparetwo )
igouy
18
  • Les performances de Scala sont très correctes si vous écrivez simplement du code semblable à Java / C dans Scala. Le compilateur utilisera primitives JVM pour Int, Charetc. quand il le peut. Les boucles While sont tout aussi efficaces en Scala.
  • Gardez à l'esprit que les expressions lambda sont compilées à des instances de sous-classes anonymes des Functionclasses. Si vous passez un lambda à map, la classe anonyme doit être instanciée (et il peut être nécessaire de transmettre certaines sections locales), puis chaque itération entraîne une surcharge supplémentaire d'appels de fonction (avec certains paramètres passés) apply.
  • De nombreuses classes comme ne scala.util.Randomsont que des wrappers autour de classes JRE équivalentes. L'appel de fonction supplémentaire est légèrement inutile.
  • Méfiez-vous des implications dans le code essentiel à la performance. java.lang.Math.signum(x)est beaucoup plus direct que x.signum(), qui convertit RichIntet retour.
  • Le principal avantage de Scala en termes de performances par rapport à Java est la spécialisation. Gardez à l'esprit que la spécialisation est utilisée avec parcimonie dans le code de la bibliothèque.
Daniel Lubarov
la source
5
  • a) De par ma connaissance limitée, je dois remarquer que le code dans la méthode principale statique ne peut pas être optimisé très bien. Vous devez déplacer le code critique vers un emplacement différent.
  • b) Après de longues observations, je recommanderais de ne pas faire de gros travaux sur les tests de performances (sauf que c'est exactement ce que vous aimez optimiser, mais qui devrait lire 2 millions de valeurs?). Vous mesurez l'impression, ce qui n'est pas très intéressant. Remplacer le println par max:
(1 to 20000).toList.map (_ * 2).max

réduit le temps de 800 ms à 20 sur mon système.

  • c) Le for-compréhension est connu pour être un peu lent (il faut bien admettre que cela ne cesse de s'améliorer). Utilisez plutôt les fonctions while ou tailrecursive. Pas dans cet exemple, où il s'agit de la boucle externe. Utilisez @ tailrec-annotation pour tester la tairécursivité.
  • d) La comparaison avec C / Assembler échoue. Vous ne réécrivez pas le code scala pour différentes architectures, par exemple. Autre différence importante par rapport aux situations historiques:
    • Compilateur JIT, optimisant à la volée, et peut-être dynamiquement, en fonction des données d'entrée
    • L'importance des ratés
    • L'importance croissante de l'invocation parallèle. Scala a aujourd'hui des solutions pour travailler sans beaucoup de frais généraux en parallèle. Ce n'est pas possible en Java, sauf que vous faites beaucoup plus de travail.
Utilisateur inconnu
la source
2
J'ai supprimé le println de la boucle et en réalité le code Scala est plus rapide que le code Java.
Giorgio
La comparaison avec C et Assembler était conçue dans le sens suivant: un langage de niveau supérieur possède des abstractions plus puissantes, mais vous devrez peut-être utiliser le langage de niveau inférieur pour obtenir des performances. Ce parallèle tient-il en considérant Scala en tant que niveau supérieur et Java en tant que langage de niveau inférieur? Peut-être que non, puisque Scala semble offrir des performances similaires à celles de Java.
Giorgio
Je ne pensais pas que cela importait beaucoup pour Clojure ou Scala, mais lorsque j’avais l'habitude de jouer avec jRuby et Jython, j'aurais probablement écrit le code le plus critique en termes de performances en Java. Avec ces deux-là, j'ai constaté une grande disparité, mais c'était il y a des années maintenant… pourrait être meilleure.
Rig