Abstrasy
2.0 (beta)

Premiers benchmarks d'Abstrasy 2.0 via la JSR-223

Alors que l’effervescence bat son plein autour de la création de la nouvelle version du langage, il est bien de prendre le temps de publier quelques résultats obtenus avec la pré-version du futur Abstrasy 2.0 qui traîne actuellement dans le laboratoire.

A ce stade, Abstrasy 2.0 n'en est toujours qu'à sa forme fœtale. Il ne possède aucune extension et les opérateurs qui équiperont le noyau final de l'interpréteur ne sont même pas encore fixés définitivement.

Cependant, il ne fait aucun doute qu'Abstrasy 2.0 sera particulièrement performant.

Ses performances sont telles qu'elles peuvent bousculer une idée reçue très fréquente : L'idée selon laquelle les programmes écrits dans un langage de programmation compilé sont toujours plus rapides que ceux écrits dans un langage interprété.

Il faut dire qu'un travail important a été réalisé dans le domaine de l'optimisation du nouveau langage en tenant compte de son utilisation dans des situations concrètes. Ainsi, Abstrasy est généralement utilisé comme interpréteur pour implémenter les aspects dynamiques d'applications Java plus importantes.

Déjà en 2008, le module B-Macro intégré au logiciel BeDesk Express Facturation 5 (un logiciel de gestion commerciale publié par la société BEDESK sprl, l'unique sponsor du projet Abstrasy) était conçu sur ce modèle. Bien entendu, à l'époque la JSR-223 n'existait pas encore. Une interface de programmation dynamique avait donc été développée en conséquence. C'est cette même interface qui a été utilisée lorsque le projet a été renommé «Abstrasy» et publié en open-source jusqu'à sa version 1.1 (version qui équipe notamment le logiciel BeDesk Express 2013).

Avec la version 2.0, nous abandonnons l'ancienne interface pour adopter également la JSR-223.

Dans cet article, nous allons présenter un test composé de 5 micro-benchmarks que nous avons réalisé sur un PC équipé d'un processeur Intel Core i7 960 à 3,20GHz. Bien que l'interpréteur soit développé en Java 7, nous avons utilisé une pré-version du JDK 8 pour compiler et exécuter le programme de test. De cette manière, nous avons pu comparer les performances d'Abstrasy 2.0 par rapport à deux moteurs JavaScript bien connus: Mozilla Rhino et Oracle Nashorn. Ces deux langages proposent un port JSR-223 et une interface Compilable.

Avant de reléguer une des idées reçues parmi les plus tenaces au rang de légende urbaine, rappelons pourquoi on peut effectivement s'attendre à de meilleures performances de la part d'un langage de programmation compilé par rapport à un langage interprété.


Un langage de programmation compilé est-il toujours plus performant qu'un langage interprété ?

Il est évident qu'un programme écrit dans un langage de programmation compilé devrait être exécuté plus rapidement qu'un programme identique écrit dans un langage interprété. Voyons pourquoi…


Pratiquement toujours dans le cas d'un langage de programmation statique...

Lorsqu'un programme est écrit dans un langage de programmation interprété, il est nécessaire d'utiliser un interpréteur pour l'exécuter. Cet interpréteur doit constamment lire et analyser chaque opération du programme pour l'exécuter. Ce contrôle continu de l'exécution pénalise donc inévitablement les performances.

Ainsi, on conçoit aisément qu'un programme préalablement compilé en code binaire exécutable sera forcément exécuté plus rapidement. En effet, non seulement son exécution sera réalisée directement par le procésseur de l'ordinateur, mais en plus, on supprime la nécessité de controler celle-ci constamment. C'est ce que l'on constate particulièrement dans des langages de programmation comme le C ou le C++. Mais, cela se vérifie aussi en Java.

En effet, la machine virtuelle Java (JVM) peut être considérée comme une sorte de «processeur fictif» qui exécute du bytecode (le code binaire exécutable spécifique de la JVM). On obtient le bytecode en compilant un programme écrit en langage Java. Pour cela, le Java Developer Kit (JDK) fourni un compilateur, il s'agit de «javac». Le bytecode résultant de la compilation du code source Java est enregistré dans un ou plusiers fichiers specifiques dont le nom se termine par «.class». Ainsi, dans l'environnement Java, le bytecode correspond, pour la JVM, au code binaire exécutable du processeur. Cette technique associée à de nombreuses optimisations permet à la JVM d'atteindre des performances très appréciables qui sont parfois très proches de celles que l'on obtient en C++.

Bien entendu, on compile un programme une fois pour toute avant de l'exécuter. Tant que le programme ne change pas, on réutilise le même bytecode. On ne compile pas un programme à chaque fois qu'on doit l'utiliser. Le code compilé est donc immuable. C'est pour cela que l'on dit de ces langages de programmation compilés, qu'il s'agit de langages de programmation «statiques».


Et pour ce qui est d'un langage de programmation dynamique ?...

Cependant, il peut arriver que certaines parties d'un programme ne puissent pas être compilées avant l'exécution. Pensez par exemple à la feuille de calculs d'un tableur. Cet exemple est suffisamment trivial pour être facile à comprendre tout en nous permettant d'en tirer des conclusions importantes.

Dans une feuille de calculs, chaque cellule peut recevoir soit une donnée ou une formule.

Lorsqu'il s'agit d'une formule, celle-ci est rédigée dans un langage de programmation que l'on doit pouvoir évaluer alors que la compilation du programme principal est terminée depuis longtemps. Il est évident qu'on ne peut pas compiler les formules de la feuille de calculs à l'avance car c'est pendant l'utilisation que l'utilisateur va les introduire lui-même et les modifier à volonté. On utilise alors un langage de programmation «dynamique» parce qu'on doit pouvoir modifier le code source à tous moment.


Rappel : Java Specification Request : JSR-223

C'est pour répondre à cette préoccupation que l'environnement Java propose la fameuse JSR-223 à partir de Java 6. A l'aide de cette API, on peut facilement implémenter l'évaluation de scripts dynamiques dans un programme compilé en Java.

//
// Portion de programme Java
// 
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("javascript");
System.out.println( engine.eval("2 + 2") );
//
// Évalue le JavaScript "2 + 2" et affiche le résultat "4" dans la console.
//

Ainsi, depuis Java 6, le moteur de script Mozilla Rhino est intégré aux paquetages de base de la distribution du JRE. Java 8 ajoute un nouveau moteur JavaScript édité par Oracle, il s'agit du moteur Nashorn.

Bien sûr, tout le monde peut ajouter son propre moteur de script. Il en existe d'ailleurs plusieurs pour différents langages de programmation bien connus: Jython pour le langage Python, JRuby pour le langage Ruby, on encore Groovy, etc… Aussi, Abstrasy 2.0 introduit son propre moteur de script conforme aux spécifications de la JSR-223.


Le lièvre et la tortue

Contrairement aux autres implémentations de langages de script précités, Abstrasy 2.0 est exclusivement interprété et ne propose pas d'interface de compilation vers du bytecode Java. Or, dans l'argumentaire généralement prôné, il est souvent convenu que les benchmarks des scripts compilés sont meilleurs que ceux des scripts interprétés.

Illustrons cela par un exemple simple qui permet de comparer un JavaScript exécuté dans un terminal par Rhino à l'aide de la commande «js» et un script similaire évalué dans la console d'Abstrasy 2.0.

Voici le JavaScript:

test1.js
print("Benchmark pour Javascript (Rhino 1.7)");
print("============================================================");
print("");
 
x = 10000000;
 
print("Test d\'une boucle while " + x + " iterations.");
print("------------------------------------------------------------");
 
mEssais = 5;
essais = mEssais;
temps = 0;
 
while(essais > 0) {
 
    deb = new Date().getTime();
 
    i = x;
    while(i > 0) { i=i-1; }
 
    fin = new Date().getTime();
    print("Temps : " + (fin-deb) + "ms.");
    temps = temps + (fin-deb);
    essais = essais - 1;
 
}
 
print("------------------------------------------------------------");
print("Temps moyen : " + (temps/mEssais)+ "ms.");

Sur notre machine de tests, nous l'exécutons dans un terminal à l'aide de la commande:

js test1.js

Et nous obtenons le résultat suivant:

Benchmark pour Javascript (Rhino 1.7)
============================================================

Test d'une boucle while 10000000 iterations.
------------------------------------------------------------
Temps : 931ms.
Temps : 916ms.
Temps : 898ms.
Temps : 853ms.
Temps : 882ms.
------------------------------------------------------------
Temps moyen : 896ms.

Éditons à présent un script similaire en Abstrasy 2.0 (même nombre d'itérations et même style de boucle):

test1.abstrasy
(display-clr)
(display "Benchmark pour Abstrasy 2.0 ")
(display "============================================================")
(display)
 
(define 'x 10000000)
 
(display "Test d'une boucle while " x " itérations.")
(display "------------------------------------------------------------")
 
(define 'essais 10)
(define '$essais essais)
(define '$temps 0)
(while{$essais}
  {
    (define 'deb (now))
 
    # Debut du test...
    (define '$i x)
    (while{$i} {set! $i (- $i 1)})
    # Fin du test...
 
    (define 'fin (now))
    (display "Temps : " (- fin deb) "ms.")
    (set! $temps (+ $temps (- fin deb)))
    (set! $essais (- $essais 1))
  }
)
(display "------------------------------------------------------------")
(display "Temps moyen : " (round (/ $temps essais)) "ms.")

Nous lançons son évaluation et nous obtenons ceci dans la console:

Benchmark pour Abstrasy 2.0 
============================================================

Test d'une boucle while 10000000 itérations.
------------------------------------------------------------
Temps : 1152ms.
Temps : 1167ms.
Temps : 1146ms.
Temps : 1147ms.
Temps : 1165ms.
Temps : 1127ms.
Temps : 1168ms.
Temps : 1140ms.
Temps : 1135ms.
Temps : 1160ms.
------------------------------------------------------------
Temps moyen : 1151ms.
Ready...

On constate effectivement que le script est moins performant en Abstrasy qu'en JavaScript. La version en Abstrasy est environ 30% moins rapide que la version JavaScript exécutée par Rhino. Ainsi, force est de constater qu'en vitesse d'exécution «pure», le moteur Rhino l'emporte sur Abstrasy.

Un vrai lièvre ce moteur Rhino!… Pour parvenir à de meilleures performances, Rhino compile préalablement le script en bytecode Java, puis exécute le bytecode obtenu. L'exécution est donc réalisée très rapidement, à la vitesse de la JVM.

Comparativement, Abstrasy ce comporte comme une tortue. Il réalise une brève analyse du script pour produire une structure de données que l'on appelle un «Arbre syntaxique abstrait» (AST), puis se met à lire cette structure pour l'analyser et l'évaluer pas à pas.


Mais, c'est bien la tortue qui est arrivée avant le lièvre!

Toutefois, notre test ne tient compte que de la vitesse exécution «pure» obtenue après compilation en bytecode ou transformation en AST. Dans le cas de l'utilisation de la JSR-223, il faut aussi tenir compte du temps passé par chaque moteur à compiler ou à analyser le code source avant l'exécution proprement dite.

Je dois avouer que j'ai fais quelques recherches sur internet sans vraiment pouvoir trouver de tests de performance qui tiennent compte de ce paramètre. Or, cette contrainte est inévitable si on utilise la JSR-223. Le temps de compilation fait immanquablement partie du temps d'exécution.

Quel est l'impact du temps de compilation dans le cadre de l'utilisation de la JSR-223?

Pour répondre à cette question, nous avons écrit un petit programme de test en Java. Implémenté en Java 8, il permet de tester 3 moteurs:

  1. Le moteur Rhino de Mozilla (JavaScript)
  2. Le nouveau moteur Nashorn d'Oracle (JavaScript) du JDK 8
  3. Ainsi que le moteur du futur Abstrasy 2.0

Les 3 moteurs sont comparés au travers de l'interface JSR-223 dans des conditions proches de celles que l'on aurait si on devait implémenter une feuille de calculs avec formules (nombreux appels relativement brefs).

Ce test tient compte du temps d'analyse et de compilation et un «ratio» indique le facteur de vitesse qu'il y a entre le moteur le plus lent et celui qui est le plus rapide. Bien sûr, le moteur le plus rapide est celui qui réalise les opérations dans le temps le plus court et le plus lent celui qui met le plus de temps pour accomplir le même travail.

Le test consistent à calculer la somme des carrés des 100000 premiers nombres entiers à l'aide de la formule:

Pour révéler les éventuelles variations de performance en fonction de la méthode utilisée, nous proposons 5 micro-benchmarks différents. En voici les caractéristiques respectives:

Benchmark #1

Nous plaçons la formule à évaluer dans une chaîne de caractères que nous envoyons au ScriptEngine à l'aide de la méthode eval(). Dans ce test, nous n'utilisons pas de variables. Nous créons donc à chaque fois une nouvelle formule qu'il faut analyser ou compiler lors de chaque itération. Aucune réutilisation n'est possible. Pour nous assurer de cela, la boucle principale est implémentée directement dans l'appelant Java. Il s'agit donc du test le plus stressant pour le ScriptEngine.

En résumé:

  • Le script à évaluer change tout le temps.
  • On n'utilise pas de variables (Bindings).
  • Le script retourne une valeur (qui est utilisée pour l'itération suivante).

Benchmark #2

Dans ce test, nous utilisons les fonctions Bindings du ScriptEngine. Nous assignons donc les valeurs aux variables que le ScriptEngine peut récupérer pour évaluer la formule. Le ScriptEngine a donc l'opportunité de réutiliser la précédente analyse du code source, puisque c'est le même script qui lui est envoyé lors de chaque itération. La somme est toutefois renvoyée à la fin de chaque itération pour être affecté à une variable d'accumulation.

En résumé:

  • Le script à évaluer est toujours le même (le moteur de script à l'opportunité de pouvoir récupérer l'analyse précédente du code source).
  • On utilise des variables Bindings en lecture seule.
  • Le script retourne une valeur (qui est utilisée pour l'itération suivante, ré-assignée à une variable Bindings).

Benchmark #3

A partir de ce test, le code source de la formule à évaluer est pré-compilé une fois pour toute avant son évaluation. Cette fonctionnalité n'étant pas disponible dans le moteur Abstrasy 2.0, nous reprenons le test précédent pour ce moteur. On ne peut donc pas s'attendre à une grosse variation pour le moteur Abstrasy 2.0 lors de cette partie du test.

En résumé:

  • Le script est immuable et est compilé à l'avance si le moteur implémente l'interface Compilable (Rhino et Nashorn). On utilise alors le code compilé et non plus le code source.
  • Abstrasy 2.0 n'implémente pas l'interface Compilable. On reprend donc le même procédé que dans le benchmark précédent.
  • On utilise des variables (Bindings) en lecture comme dans le benchmark précédent.
  • Le script retourne une valeur (qui est utilisée pour l'itération suivante).

Benchmark #4

Au cours de cette partie du test, on réalise l'affectation d'une variable qui sert d'accumulateur. Il s'agit de la variable «o». On récupère sa valeur uniquement à la fin du test. Le moteur ne doit donc plus renvoyer de résultat lors de chaque itération.

On notera toutefois qu'en Abstrasy 2.0, on ne peut pas changer la valeur d'une variable. On utilise donc une référence dont le contenu peut varier pour obtenir le même résultat (c'est une méthode similaire a celle que propose le langage OCaml).

En résumé:

  • On reprend les caractéristiques du test précédent : Script compilé à l'avance avant l'évaluation si Compilable.
  • On utilise à présent uniquement des variables (Bindings). La valeur de retour n'est pas renvoyée par le script, mais affectée à une variable (en l'occurence «o»).
  • On note toutefois qu'Abstrasy 2.0 ne permet pas l'affectation destructive des variables. On utilise donc une référence.
  • Le script ne retourne plus de valeur.

Benchmark #5

Enfin, le test où les langages compilés pourront bénéficier au maximum de l'optimisation qui résulte de la compilation. La boucle est totalement implémentée dans le script et un seul appel est réalisé pour évaluer la somme après l'avoir compilé une fois pour toute. Toutefois, pour respecter les conditions du test, le coup de la compilation fait partie de la mesure du temps. Il s'agit de la partie du test où le moteur Abstrasy 2.0 sera le moins avantagé.

En résumé:

  • Le script est préalablement compilé avant l'évaluation si le moteur de script implémente l'interface Compilable. C'est le cas pour Rhino et Nashorn, mais pas pour Abstrasy 2.0 où l'évaluation ne peut être qu'interprétée. Le temps de compilation est toutefois compté.
  • La boucle n'est plus implémentée par l'appelant Java, mais bien dans le script lui-même à l'aide du'une boucle du type «while».
  • On utilise uniquement des variables (Bindings).

Le code source du test complet

Voici le code source du test:

Benchmarks1.java
package jsr223;
 
 
import abstrasy.Node_float;
import abstrasy.Node_ref;
 
import java.util.List;
 
import javax.script.Compilable;
import javax.script.CompiledScript;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineFactory;
import javax.script.ScriptEngineManager;
 
 
public class Benchmarks1 {
 
    public Benchmarks1() {
    }
 
 
    private static int B1_LOOPS = 100000;
 
    private static double b5_nashorn(ScriptEngineManager manager) {
        double t0 = 0;
        try {
            ScriptEngine engine = manager.getEngineByName("nashorn");
            System.out.println("   * Benchmarking Nashorn engine...");
            long s = System.currentTimeMillis();
            Compilable engineCompilable = (Compilable) engine;
            CompiledScript scriptCompile = engineCompilable.compile("while(i>0){ o = Math.sqrt(i) + o; i = i - 1; }");
            engine.put("o", 0);
            engine.put("i", B1_LOOPS);
            scriptCompile.eval();
            Object o = engine.get("o");
            long e = System.currentTimeMillis();
            t0 = e - s;
            System.out.println("     Last Result : " + o);
            System.out.println("     Time : " + t0 + "ms.");
        }
        catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println();
        return t0;
    }
 
    private static double b5_rhino(ScriptEngineManager manager) {
        double t0 = 0;
        try {
            ScriptEngine engine = manager.getEngineByName("rhino");
            System.out.println("   * Benchmarking Rhino engine...");
            long s = System.currentTimeMillis();
            Compilable engineCompilable = (Compilable) engine;
            CompiledScript scriptCompile = engineCompilable.compile("while(i>0){ o = Math.sqrt(i) + o; i = i - 1; }");
            engine.put("o", 0);
            engine.put("i", B1_LOOPS);
            scriptCompile.eval();
            Object o = engine.get("o");
            long e = System.currentTimeMillis();
            t0 = e - s;
            System.out.println("     Last Result : " + o);
            System.out.println("     Time : " + t0 + "ms.");
        }
        catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println();
        return t0;
    }
 
 
    private static double b5_abstrasy(ScriptEngineManager manager) {
        double t0 = 0;
        try {
            ScriptEngine engine = manager.getEngineByName("abstrasy");
            System.out.println("   * Benchmarking Abstrasy 2.0 engine...");
            long s = System.currentTimeMillis();
            engine.put("$o", new Node_ref(new Node_float(0),null));
            engine.put("$i", new Node_ref(new Node_float(B1_LOOPS),null));
            engine.eval("(while{$i} {(set! $o (+ (sqrt $i) $o))(set! $i (- $i 1))})");
            Object o = engine.get("$o");
            long e = System.currentTimeMillis();
            t0 = e - s;
            System.out.println("     Last Result : " + o + ", soit: " + ((Node_ref) o).getRef_unsafe());
            System.out.println("     Time : " + t0 + "ms.");
        }
        catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println();
        return t0;
    }
 
 
 
 
    private static double b4_nashorn(ScriptEngineManager manager) {
        double t0 = 0;
        try {
            ScriptEngine engine = manager.getEngineByName("nashorn");
            System.out.println("   * Benchmarking Nashorn engine...");
            long s = System.currentTimeMillis();
            Compilable engineCompilable = (Compilable) engine;
            CompiledScript scriptCompile = engineCompilable.compile("o = Math.sqrt(i) + o");
            Object o = new Integer(0);
            for (int i = B1_LOOPS; i>0 ; i--) {
                engine.put("o", o);
                engine.put("i", i);
                scriptCompile.eval();
                o = engine.get("o");
            }
            long e = System.currentTimeMillis();
            t0 = e - s;
            System.out.println("     Last Result : " + o);
            System.out.println("     Time : " + t0 + "ms.");
        }
        catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println();
        return t0;
    }
 
    private static double b4_rhino(ScriptEngineManager manager) {
        double t0 = 0;
        try {
            ScriptEngine engine = manager.getEngineByName("rhino");
            System.out.println("   * Benchmarking Rhino engine...");
            long s = System.currentTimeMillis();
            Compilable engineCompilable = (Compilable) engine;
            CompiledScript scriptCompile = engineCompilable.compile("o = Math.sqrt(i) + o");
            Object o = new Integer(0);
            for (int i = B1_LOOPS; i>0 ; i--){
                engine.put("o", o);
                engine.put("i", i);
                scriptCompile.eval();
                o = engine.get("o");
            }
            long e = System.currentTimeMillis();
            t0 = e - s;
            System.out.println("     Last Result : " + o);
            System.out.println("     Time : " + t0 + "ms.");
        }
        catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println();
        return t0;
    }
 
 
    private static double b4_abstrasy(ScriptEngineManager manager) {
        double t0 = 0;
        try {
            ScriptEngine engine = manager.getEngineByName("abstrasy");
            System.out.println("   * Benchmarking Abstrasy 2.0 engine...");
            long s = System.currentTimeMillis();
            Object o = new Node_ref(new Node_float(0),null);
            for (int i = B1_LOOPS; i>0 ; i--) {
                engine.put("$o", o);
                engine.put("i", new Node_float(i));
                engine.eval("(set! $o (+ (sqrt i) $o))");
                o = engine.get("$o");
            }
            long e = System.currentTimeMillis();
            t0 = e - s;
            System.out.println("     Last Result : " + o + ", soit: " + ((Node_ref) o).getRef_unsafe());
            System.out.println("     Time : " + t0 + "ms.");
        }
        catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println();
        return t0;
    }
 
    private static double b3_nashorn(ScriptEngineManager manager) {
        double t0 = 0;
        try {
            ScriptEngine engine = manager.getEngineByName("nashorn");
            System.out.println("   * Benchmarking Nashorn engine...");
            long s = System.currentTimeMillis();
            Compilable engineCompilable = (Compilable) engine;
            CompiledScript scriptCompile = engineCompilable.compile("Math.sqrt(i) + o");
            Object o = new Integer(0);
            for (int i = B1_LOOPS; i>0 ; i--) {
                engine.put("o", o);
                engine.put("i", i);
                o = scriptCompile.eval();
            }
            long e = System.currentTimeMillis();
            t0 = e - s;
            System.out.println("     Last Result : " + o);
            System.out.println("     Time : " + t0 + "ms.");
        }
        catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println();
        return t0;
    }
 
 
    private static double b3_rhino(ScriptEngineManager manager) {
        double t0 = 0;
        try {
            ScriptEngine engine = manager.getEngineByName("rhino");
            System.out.println("   * Benchmarking Rhino engine...");
            long s = System.currentTimeMillis();
            Compilable engineCompilable = (Compilable) engine;
            CompiledScript scriptCompile = engineCompilable.compile("Math.sqrt(i) + o");
            Object o = new Integer(0);
            for (int i = B1_LOOPS; i>0 ; i--) {
                engine.put("o", o);
                engine.put("i", i);
                o = scriptCompile.eval();
            }
            long e = System.currentTimeMillis();
            t0 = e - s;
            System.out.println("     Last Result : " + o);
            System.out.println("     Time : " + t0 + "ms.");
        }
        catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println();
        return t0;
    }
 
    private static double b3_abstrasy(ScriptEngineManager manager) {
        double t0 = 0;
        try {
            ScriptEngine engine = manager.getEngineByName("abstrasy");
            System.out.println("   * Benchmarking Abstrasy 2.0 engine...");
            long s = System.currentTimeMillis();
            Object o = new Node_float(0);
            for (int i = B1_LOOPS; i>0 ; i--) {
                engine.put("o", o);
                engine.put("i", new Node_float(i));
                o = engine.eval("(+ (sqrt i) o)");
            }
            long e = System.currentTimeMillis();
            t0 = e - s;
            System.out.println("     Last Result : " + o);
            System.out.println("     Time : " + t0 + "ms.");
        }
        catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println();
        return t0;
    }
 
 
    private static double b2_nashorn(ScriptEngineManager manager) {
        double t0 = 0;
        try {
            ScriptEngine engine = manager.getEngineByName("nashorn");
            System.out.println("   * Benchmarking Nashorn engine...");
            long s = System.currentTimeMillis();
            Object o = new Integer(0);
            for (int i = B1_LOOPS; i>0 ; i--) {
                engine.put("o", o);
                engine.put("i", i);
                o = engine.eval("Math.sqrt(i) + o");
            }
            long e = System.currentTimeMillis();
            t0 = e - s;
            System.out.println("     Last Result : " + o);
            System.out.println("     Time : " + t0 + "ms.");
        }
        catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println();
        return t0;
    }
 
    private static double b2_rhino(ScriptEngineManager manager) {
        double t0 = 0;
        try {
            ScriptEngine engine = manager.getEngineByName("rhino");
            System.out.println("   * Benchmarking Rhino engine...");
            long s = System.currentTimeMillis();
            Object o = new Integer(0);
            for (int i = B1_LOOPS; i>0 ; i--) {
                engine.put("o", o);
                engine.put("i", i);
                o = engine.eval("Math.sqrt(i) + o");
            }
            long e = System.currentTimeMillis();
            t0 = e - s;
            System.out.println("     Last Result : " + o);
            System.out.println("     Time : " + t0 + "ms.");
        }
        catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println();
        return t0;
    }
 
 
    private static double b2_abstrasy(ScriptEngineManager manager) {
        double t0 = 0;
        try {
            ScriptEngine engine = manager.getEngineByName("abstrasy");
            System.out.println("   * Benchmarking Abstrasy 2.0 engine...");
            long s = System.currentTimeMillis();
            Object o = new Node_float(0);
            for (int i = B1_LOOPS; i>0 ; i--) {
                engine.put("o", o);
                engine.put("i", new Node_float(i));
                o = engine.eval("(+ (sqrt i) o)");
            }
            long e = System.currentTimeMillis();
            t0 = e - s;
            System.out.println("     Last Result : " + o);
            System.out.println("     Time : " + t0 + "ms.");
        }
        catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println();
        return t0;
    }
 
    private static double b1_nashorn(ScriptEngineManager manager) {
        double t0 = 0;
        try {
            ScriptEngine engine = manager.getEngineByName("nashorn");
            System.out.println("   * Benchmarking Nashorn engine...");
            long s = System.currentTimeMillis();
            Object o = new Double(0);
            for (int i = B1_LOOPS; i>0 ; i--)
                o = engine.eval("Math.sqrt(" + i + ") + " + o);
            long e = System.currentTimeMillis();
            t0 = e - s;
            System.out.println("     Last Result : " + o);
            System.out.println("     Time : " + t0 + "ms.");
        }
        catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println();
        return t0;
    }
 
    private static double b1_rhino(ScriptEngineManager manager) {
        double t0 = 0;
        try {
            ScriptEngine engine = manager.getEngineByName("rhino");
            System.out.println("   * Benchmarking Rhino engine...");
            long s = System.currentTimeMillis();
            Object o = new Double(0);
            for (int i = B1_LOOPS; i>0 ; i--)
                o = engine.eval("Math.sqrt(" + i + ") + " + o);
            long e = System.currentTimeMillis();
            t0 = e - s;
            System.out.println("     Last Result : " + o);
            System.out.println("     Time : " + t0 + "ms.");
        }
        catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println();
        return t0;
    }
 
    private static double b1_abstrasy(ScriptEngineManager manager) {
        double t0 = 0;
        try {
            ScriptEngine engine = manager.getEngineByName("abstrasy");
            System.out.println("   * Benchmarking Abstrasy 2.0 engine...");
            long s = System.currentTimeMillis();
            Object o = new Double(0);
            for (int i = B1_LOOPS; i>0 ; i--)
                o = engine.eval("(+ (sqrt " + ((double)i) + ") " + o + ")");
            long e = System.currentTimeMillis();
            t0 = e - s;
            System.out.println("     Last Result : " + o);
            System.out.println("     Time : " + t0 + "ms.");
        }
        catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println();
        return t0;
    }
 
    private static final double MAX(double... v) {
        double r = v[0];
        for (int i = 0; i < v.length; i++)
            r = Math.max(r, v[i]);
        return r;
    }
 
    private static final double MIN(double... v) {
        double r = v[0];
        for (int i = 0; i < v.length; i++)
            r = Math.min(r, v[i]);
        return r;
    }
 
    public static void main(String[] args) {
        ScriptEngineManager manager = new ScriptEngineManager();
 
        List<ScriptEngineFactory> factories = manager.getEngineFactories();
 
        System.out.println("(JSR-223) Liste des ScriptEngines disponibles:");
        System.out.println("=============================================================================");
        System.out.println();
        for (ScriptEngineFactory factory: factories) {
 
            System.out.println("  " + factory.getEngineName());
            System.out.println("  ---------------------------------------------------------------------------");
            System.out.println("              Name : " + factory.getEngineName());
            System.out.println("           Version : " + factory.getEngineVersion());
            System.out.println("     Language name : " + factory.getLanguageName());
            System.out.println("  Language version : " + factory.getLanguageVersion());
            System.out.println("        Extensions : " + factory.getExtensions());
            System.out.println("        Mime types : " + factory.getMimeTypes());
            System.out.println("             Names : " + factory.getNames());
            System.out.println();
 
        }
        System.out.println("  ---------------------------------------------------------------------------");
        System.out.println();
        System.out.println();
 
        boolean hasNashorn = manager.getEngineByName("nashorn") != null;
 
        System.out.println("Benchmark #1:");
        System.out.println("=============================================================================");
        System.out.println();
        double t1 = b1_rhino(manager);
        double t2 = hasNashorn ? b1_nashorn(manager) : t1;
        double t0 = b1_abstrasy(manager);
        System.out.println("   => Ratio : " + (MAX(t0, t1, t2) / MIN(t0, t1, t2)));
        System.out.println("  ---------------------------------------------------------------------------");
        System.out.println();
        System.out.println();
 
        System.out.println("Benchmark #2:");
        System.out.println("=============================================================================");
        System.out.println();
        t1 = b2_rhino(manager);
        t2 = hasNashorn ? b2_nashorn(manager) : t1;
        t0 = b2_abstrasy(manager);
        System.out.println("   => Ratio : " + (MAX(t0, t1, t2) / MIN(t0, t1, t2)));
        System.out.println("  ---------------------------------------------------------------------------");
        System.out.println();
        System.out.println();
 
        System.out.println("Benchmark #3:");
        System.out.println("=============================================================================");
        System.out.println();
        t1 = b3_rhino(manager);
        t2 = hasNashorn ? b3_nashorn(manager) : t1;
        t0 = b3_abstrasy(manager);
        System.out.println("   => Ratio : " + (MAX(t0, t1, t2) / MIN(t0, t1, t2)));
        System.out.println("  ---------------------------------------------------------------------------");
        System.out.println();
        System.out.println();
 
        System.out.println("Benchmark #4:");
        System.out.println("=============================================================================");
        System.out.println();
        t1 = b4_rhino(manager);
        t2 = hasNashorn ? b4_nashorn(manager) : t1;
        t0 = b4_abstrasy(manager);
        System.out.println("   => Ratio : " + (MAX(t0, t1, t2) / MIN(t0, t1, t2)));
        System.out.println("  ---------------------------------------------------------------------------");
        System.out.println();
        System.out.println();
 
        System.out.println("Benchmark #5:");
        System.out.println("=============================================================================");
        System.out.println();
        t1 = b5_rhino(manager);
        t2 = hasNashorn ? b5_nashorn(manager) : t1;
        t0 = b5_abstrasy(manager);
        System.out.println("   => Ratio : " + (MAX(t0, t1, t2) / MIN(t0, t1, t2)));
        System.out.println("  ---------------------------------------------------------------------------");
        System.out.println();
        System.out.println();
    }
 
}

Passons au test proprement-dit

Compilons ce programme et exécutons-le sur Java 8. Nous obtenons ceci dans la console:

(JSR-223) Liste des ScriptEngines disponibles:
=============================================================================

  Mozilla Rhino
  ---------------------------------------------------------------------------
              Name : Mozilla Rhino
           Version : 1.7 release 3 PRERELEASE
     Language name : ECMAScript
  Language version : 1.8
        Extensions : [js]
        Mime types : [application/javascript, application/ecmascript, text/javascript, text/ecmascript]
             Names : [js, rhino, JavaScript, javascript, ECMAScript, ecmascript]

  Oracle Nashorn
  ---------------------------------------------------------------------------
              Name : Oracle Nashorn
           Version : 1.8.0
     Language name : ECMAScript
  Language version : ECMA - 262 Edition 5.1
        Extensions : [js]
        Mime types : [application/javascript, application/ecmascript, text/javascript, text/ecmascript]
             Names : [nashorn, Nashorn, js, JS, JavaScript, javascript, ECMAScript, ecmascript]

  Abstrasy 2.0 Engine
  ---------------------------------------------------------------------------
              Name : Abstrasy 2.0 Engine
           Version : 0.1.0
     Language name : Abstrasy
  Language version : 2.0_36.0
        Extensions : [abstrasy]
        Mime types : [application/abstrasy, text/abstrasy]
             Names : [abstrasy, Abstrasy]

  ---------------------------------------------------------------------------


Benchmark #1:
=============================================================================

   * Benchmarking Rhino engine...
     Last Result : 2.108200897391742E7
     Time : 8570.0ms.

   * Benchmarking Nashorn engine...
     Last Result : 2.108200897391742E7
     Time : 87848.0ms.

   * Benchmarking Abstrasy 2.0 engine...
     Last Result : 2.108200897391742E7
     Time : 860.0ms.

   => Ratio : 102.14883720930233
  ---------------------------------------------------------------------------


Benchmark #2:
=============================================================================

   * Benchmarking Rhino engine...
     Last Result : 2.108200897391742E7
     Time : 6362.0ms.

   * Benchmarking Nashorn engine...
     Last Result : 2.108200897391742E7
     Time : 109342.0ms.

   * Benchmarking Abstrasy 2.0 engine...
     Last Result : 2.108200897391742E7
     Time : 253.0ms.

   => Ratio : 432.1818181818182
  ---------------------------------------------------------------------------


Benchmark #3:
=============================================================================

   * Benchmarking Rhino engine...
     Last Result : 2.108200897391742E7
     Time : 5302.0ms.

   * Benchmarking Nashorn engine...
     Last Result : 2.108200897391742E7
     Time : 1918.0ms.

   * Benchmarking Abstrasy 2.0 engine...
     Last Result : 2.108200897391742E7
     Time : 199.0ms.

   => Ratio : 26.64321608040201
  ---------------------------------------------------------------------------


Benchmark #4:
=============================================================================

   * Benchmarking Rhino engine...
     Last Result : 2.108200897391742E7
     Time : 5457.0ms.

   * Benchmarking Nashorn engine...
     Last Result : 2.108200897391742E7
     Time : 2115.0ms.

   * Benchmarking Abstrasy 2.0 engine...
     Last Result : (ref 2.108200897391742E7), soit: 2.108200897391742E7
     Time : 180.0ms.

   => Ratio : 30.316666666666666
  ---------------------------------------------------------------------------


Benchmark #5:
=============================================================================

   * Benchmarking Rhino engine...
     Last Result : 2.108200897391742E7
     Time : 441.0ms.

   * Benchmarking Nashorn engine...
     Last Result : 2.108200897391742E7
     Time : 170.0ms.

   * Benchmarking Abstrasy 2.0 engine...
     Last Result : (ref 2.108200897391742E7), soit: 2.108200897391742E7
     Time : 106.0ms.

   => Ratio : 4.160377358490566
  ---------------------------------------------------------------------------


Process exited with exit code 0.


Analyse des résultats

Contrairement à ce que l'on pourrait penser, au cours de ce test, on constate que les performances du moteur Abstrasy 2.0 sont hallucinantes par rapport à celles des autres moteurs proposés dans le JDK8.

Qu'est-ce qui peut expliquer de telles performances?


Résultats du benchmark #1

Dans ce test, le moteur de script est obligé d'analyser le code source qui lui est envoyé lors de chaque itération.

Ce test est très pénalisant pour Nashorn. Rhino fait mieux, mais Abstrasy 2.0 est le plus rapide.

On peut comprendre assez aisément ce résultat. On peut même dire qu'il était tout-à-fait prévisible. En effet, contrairement aux autres langages testés, Abstrasy propose une syntaxe nettement plus simple à analyser. Ainsi, même si l'analyseur syntaxique de la version d'Abstrasy 2.0 testée n'est pas encore finalisé, ses performances sont déjà nettement meilleures que celles des autres langages aux formes syntaxiques plus complexes.


Résultats du benchmark #2

Dans ce test, on commence à utiliser l'interface Bindings des ScriptEngines. Ainsi, on peut révéler si le moteur de script est optimisé pour être en mesure de pouvoir réutiliser un code source déjà analysé (puisque qu'il ne change plus - c'est toujours le même lors de chaque itération).

On constate que l'amélioration des performances de Rhino et de Nashorn est relativement faible au regard de celle opérée par Abstrasy 2.0.

En effet, Rhino n'améliore ses performances que de 25%, alors qu'Abstrasy 2.0 multiplie sa vitesse par 4. Ne parlons pas de Nashorn qui lui semble s'enfoncer d'avantage dans l'usine à gaz.


Résultats des benchmarks #3 et #4

A partir de ce test, on utilise l'interface Compilable des ScriptEngines qui l'implémentent. C'est le cas de Rhino et de Nashorn. Par contre, Abstrasy 2.0 n'implémente pas cette interface, on peut donc s'attendre à ce qu'il soit pénalisé dans ce test.

Rappelons que l'interface Compilable de la JSR-223 permet à un ScriptEngine de compiler à l'avance un script en bytecode. C'est alors le bytecode qui est évalué. On élimine ainsi le temps de compilation ou d'analyse nécessaire avant l'évaluation proprement-dite. Si en plus, le bytecode peut être réutilisé, on peut raisonnablement penser que c'est le moyen le plus efficace d'obtenir de bonnes performances.

Ainsi, effectivement, on constate dans ce test que Nashorn remonte et fourni des performances nettement meilleures.

Rhino, quant à lui n'améliore pas significativement ses performances dans ce test. On en trouve l'explication en consultant le code source du paquetage (dans l'OpenJDK). En effet, bien que Rhino implémente l'interface Compilable, le code source n'est pas vraiment compilé. Il s'agit plus précisément d'une interface de compatibilité dans ce cas.

Abstarsy 2.0 reste plus ou moins dans le même registre de performances. On peut considérer que les variations minimes indiquées sont le résultat du travail du Garbage Collector de la JVM.

Ce qui peut surprendre, c'est qu'Abstrasy reste toujours le meilleur malgré qu'il n'implémente pas l'interface Compilable. Qu'est-ce qui peut expliquer cela ?

En fait, nous en avons déjà parlé plus haut. L'analyseur syntaxique d'Abstrasy 2.0 est très simple. Il peut donc effectuer son analyse très rapidement. Par contre, la somme de travail requise pour compiler un script est assez importante. Elle représente un coût qui est habituellement négligé, mais qui est bien présent dans le cas de l'utilisation de la JSR-223.

Pour quelle raison le coût de compilation est-il négligé par les développeurs?

Habituellement, on compile un programme une fois pour toute lors du développement de celui-ci. Après la phase de déploiement, il n'y a plus aucune opération de compilation. On ne fait plus que d'utiliser les fichiers exécutables. Le développeur n'est donc pas habitué à considérer le temps de compilation pendant l'utilisation d'un programme. Or, dans le cas de la JSR-223, ce temps de compilation compte bel et bien.


Résultats du benchmark #5

Dans ce test, nous avons voulu voir si le temps de compilation pouvait être récupéré facilement sur le temps d'évaluation.

En d'autres termes, Nashorn serait le lièvre et Abstrasy 2.0 la tortue. Abstrasy commence effectivement l'évaluation avant Nashorn, mais Nashorn est capable d'évaluer plus rapidement qu'Abstrasy.

Le lièvre arrive t-il avant la tortue?

Et bien non, à notre plus grande surprise!…

Même après 100000 itérations, Abstrasy 2.0 est toujours légèrement en tête.


En conclusion

On peut donc en conclure que le temps de compilation des langages de script qui implémentent la JSR-223 ne doit absolument pas être négligé. La nécessité de compiler les scripts représente, dans ce cas particulier d'utilisation, une pénalité importante.

Comme on l'a vu lors du benchmark #5, même après plus de 100000 itérations, Nashorn n'a pas encore rattrapé Abstrasy 2.0.

Il est donc tout-à-fait approprié de publier ce test alors que le développement d'Abstrasy 2.0 est toujours en cours. Ce test est très motivant et fourni également des informations précieuses à propos des performances liées à l'utilisation de la JSR-223.

Nous n'avons pas la prétention de vouloir prouver qu'Abstrasy 2.0 est le ScriptEngine le plus rapide. Nous avons pris en compte des paramètres et un cadre d'utilisation de la JSR-223 proche de la réalité (similaire à ceux que l'on aurait si on développait une feuille de calculs, par exemple). En effet, s'il est possible de prévoir à l'avance les parties d'un programme correspondantes aux scripts, il n'y a aucun intérêt à les implémenter sous la forme de scripts. Il est de loin plus appréciable de les développer directement en Java. C'est plus facile et plus performant.

De plus, si de telles parties devaient également représenter des segments d'exécution longs, il serait alors plus avantageux d'implémenter ces segments sous la forme de composants que l'on peut appeler et lancer sans nécessiter de compilation durant le RunTime. C'est aussi ce que permet de faire Abstrasy 2.0. Dans ce cas particulier, il sert de langage “pot de colle” (un peu comme TCL). Mais nous en reparlerons certainement dans un autre article.

articles/genese20130405.txt · Dernière modification: 2013/09/11 15:33 (modification externe)

Retour
Table des matires

 

     
Licence Creative Commons
   Get abstrasy at SourceForge.net. Fast, secure and Free Open Source software downloads