web-dev-qa-db-fra.com

Performances de Scala par rapport à Java

Tout d'abord, je voudrais préciser que ce n'est pas une question de langue X contre langue Y pour 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: à part des choses mineures qui prennent un peu l'habitude de mon impression est que je peux vraiment très bien travailler dans cette langue.

Ma question est: 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, c'est un question à répondre en général, mais je m'attendrais à ce que les constructions de niveau supérieur telles que la correspondance de motifs, les fonctions d'ordre supérieur, etc., introduisent une surcharge.

Cependant, mon expérience actuelle dans Scala est limitée à de petits exemples sous 50 lignes de code et je n'ai pas exécuté de benchmarks jusqu'à présent. Donc, je n'ai pas de données réelles.

S'il s'est avéré que Scala a du temps système par rapport à Java, est-il logique d'avoir mélangé Scala/Java, où l'on code les parties les plus complexes dans Scala et les parties critiques pour les performances en Java? Est-ce un pratique courante?

MODIFIER 1

J'ai exécuté un petit benchmark: construire une liste d'entiers, multiplier chaque entier par deux et le mettre dans une nouvelle liste, imprimer la liste résultante. J'ai écrit une implémentation Java (Java 6) et Scala (Scala 2.9). J'ai exécuté les deux sur Eclipse Indigo sous Ubuntu 10.04.

Les résultats sont comparables: 480 ms pour Java et 493 ms pour Scala (en moyenne sur 100 itérations). Voici les extraits de code 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))
}

Donc, dans ce cas, il semble que le Scala overhead (en utilisant range, map, lambda) est vraiment minime, 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?

MODIFIER 2

Certains d'entre vous ont souligné que l'impression dans les boucles internes prend la plupart du temps d'exécution. Je les ai supprimés et défini la taille des listes à 100000 au lieu de 20000. La moyenne résultante était de 88 ms pour Java et 49 ms pour Scala.

41
Giorgio

Il y a une chose que vous pouvez faire de manière concise et efficace dans Java que vous ne pouvez pas dans Scala: les énumérations. Pour tout le reste, même pour les constructions qui sont 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 y a souvent une solution dans Scala qui est adéquate ou bonne - je place l'exception sur les énumérations qui ont des méthodes supplémentaires et dont les valeurs constantes int sont utilisées.

Quant à ce à quoi faire attention, voici quelques choses.

  • Si vous utilisez le modèle d'enrichir ma bibliothèque, convertissez 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 polymorphes pour la plupart, JVM ne les optimise pas. Vous n'avez pas besoin de les éviter, mais faites attention aux sections critiques. Sachez que for dans Scala est implémenté via des appels de méthode et des classes anonymes.

  • Si vous utilisez une classe Java, telle que les classes String, Array ou AnyVal qui correspondent aux primitives Java, préférez les méthodes fourni par Java lorsqu'il existe des alternatives. Par exemple, utilisez length sur String et Array au lieu de size.

  • Évitez l'utilisation imprudente des conversions implicites, car vous pouvez vous retrouver à utiliser des conversions par erreur plutôt que par conception.

  • Étendez les classes au lieu des traits. Par exemple, si vous étendez Function1, Étendez plutôt AbstractFunction1.

  • Utilisez -optimise Et la spécialisation pour obtenir la plupart de Scala.

  • Comprenez ce qui se passe: javap est votre ami, tout comme un tas de drapeaux Scala qui montrent ce qui se passe.

  • Les idiomes Scala sont conçus pour améliorer l'exactitude et rendre le code plus concis et plus facile à gérer. Ils ne sont pas conçus pour la vitesse, donc si vous devez utiliser null au lieu de Option dans 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 est l'exécution de code. Voir cette question pour un exemple de ce qui peut arriver si vous ignorez cette règle.

39
Daniel C. Sobral

Selon le Benchmarks Game pour un seul système 32 bits, Scala est à une médiane 80% aussi rapide que Java. Les performances sont approximativement les mêmes pour un Ordinateur Quad Core x64. Même tilisation de la mémoire et densité du code sont très similaires dans la plupart des cas. Je dirais que d'après ces analyses (plutôt non scientifiques), vous avez raison d'affirmer que Scala ajoute des frais généraux à Java. Il ne semble pas ajouter des tonnes de frais généraux, donc je soupçonne que le diagnostic d'articles d'ordre supérieur prenant plus d'espace/temps est le plus correct.

21
World Engineer
  • Les performances de Scala sont très correctes si vous écrivez simplement du code de type Java/C dans Scala. Le compilateur utilisera les primitives JVM pour Int, Char, etc. quand il le pourra. Alors que les boucles sont tout aussi efficaces dans Scala.
  • Gardez à l'esprit que les expressions lambda sont compilées en instances de sous-classes anonymes des classes Function. Si vous passez un lambda à map, la classe anonyme doit être instanciée (et certaines sections locales peuvent avoir besoin d'être passées), puis chaque itération a une surcharge supplémentaire d'appel de fonction (avec un passage de paramètre) à partir du apply appels.
  • De nombreuses classes comme scala.util.Random Ne sont 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 critique pour les performances. Java.lang.Math.signum(x) est beaucoup plus direct que x.signum(), qui se convertit en RichInt et vice-versa.
  • Le principal avantage de performance de Scala par rapport à Java est la spécialisation. Gardez à l'esprit que la spécialisation est utilisée avec parcimonie dans le code de bibliothèque.
18
Daniel Lubarov
  • a) De 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) D'après de longues observations, je recommanderais de ne pas faire de sortie lourde sur le test de performance (sauf que c'est exactement ce que vous aimez optimiser, mais qui devrait jamais lire 2 millions de valeurs?). Vous mesurez l'impression, ce qui n'est pas très intéressant. Remplacement de l'impression par max:
(1 to 20000).toList.map (_ * 2).max

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

  • c) La for-comprehension est connue pour être un peu lente (alors que nous devons admettre qu'elle s'améliore tout le temps). Utilisez à la place des fonctions while ou tailrecursive. Pas dans cet exemple, où il s'agit de la boucle externe. Utilisez l'annotation @ tailrec pour tester la tairecursiveness.
  • d) La comparaison avec C/Assembler échoue. Vous ne réécrivez pas scala code pour différentes architectures par exemple. D'autres différences importantes par rapport aux situations historiques sont
    • Compilateur JIT, optimisation à la volée, et peut-être dynamiquement, en fonction des données d'entrée
    • L'importance des échecs de cache
    • L'importance croissante de l'invocation parallèle. Scala a aujourd'hui des solutions pour travailler sans trop de surcharge en parallèle. Ce n'est pas possible en Java, sauf que vous faites beaucoup plus de travail.
5
user unknown