:: Enseignements :: ESIPE :: E4INFO :: 2019-2020 :: Java Inside ::
[LOGO]

Switch on String et Tests paramétrés


Inlining Cache, MethodHandle, MutableCallSite, Test JUnit Paramétré, JMH

Exercice 1 - Switch on strings

On se propose de regarder comment un switch sur des Strings est implantée en Java.

  1. Dans le répertoire java-inside, utiliser Maven pour générer les fichiers pour le lab5
          mvn archetype:generate \
            -DarchetypeArtifactId=maven-archetype-quickstart \
            -DgroupId=fr.umlv.java.inside \
            -DartifactId=lab5  \
            -DinteractiveMode=false
        

    Recopier les properties, dependencies et les plugins de build du pom.xml du lab précédent.
  2. Créer un projet Eclipse/IntelliJ Maven pointant sur le répertoire lab5 et configurer votre IDE.
  3. Ecrire une classe de test JUnit fr.umlv.java.inside.lab5.StringSwitchExampleTests qui test une méthode stringSwitch dans une classe fr.umlv.java.inside.lab5.StringSwitchExample qui prend en paramètre une chaine de caractère et renvoie 0 pour la chaine de caractère "foo", 1 pour la chaine de caractère "bar", et 2 pour la chaine de caractère "bazz". Pour toute les autres chaines de caractère la valeur renvoyée est -1.
    Note: de même que pour le TP précédent, on vous demande d'écrire les tests avant d'écrire le code (TDD).
  4. Ecrire la méthode stringSwitch et vérifié que les tests que vous avez écrit précédemment passent.
  5. Dans l'exercice suivant, on va introduire une méthode stringSwitch2 avec une autre implantation que celle de stringSwitch, avant d'écrire le code, on souhaite faire en sorte que les tests testent non plus uniquement la méthode stringSwitch mais aussi stringSwitch2.
    Bien sûr, on pourrait dupliquer les tests mais il y a plus simple pour la maintenance, créer des tests paramétrées (JUnit 5 Parameterized Test) par la méthode la méthode à tester (sous forme de lambda).
    Dans un premier temps, ajouter la dépendance dans le fichier pom.xml.
    Puis modifier les tests pour prendre ne paramètre la méthode à tester et enfin faire en sorte que la méthode stringSwitch soit testée.
    Note: l'étape qui consiste à modifier du code dans le but de plus tard introduire une nouvelle fonctionnalité est appelé refactoring.
  6. Modifier la configuration de Travis pour exécuter les tests du lab5.

Exercice 2 - Inlining Cache

En Java, le switch sur les String fait un switch sur le hashCode des Strings ce qui oblige le compilateur et la machine virtuelle à utiliser le même algorithme pour String.hashCode.
On souhaite éviter ce problème même si cela veut dire avoir un code un peut moins efficace.
Pour cela on va écrire une méthode générique createMHFromStrings2 qui prend en paramètre des String séparées par des virgules et renvoie un method handle qui lorsqu'il est exécuté avec une chaine de caractère renvoie l'index de la chaine si c'est un argument ou -1 sinon.
    var mh = createMHFromStrings2("foo", "bar", "bazz");
    (int)mh.invokeExact("foo")    // 0
    (int)mh.invokeExact("bar")    // 1
    (int)mh.invokeExact("bazz")   // 2
    (int)mh.invokeExact("booze")  // -1
   

  1. Ecrire le code de la méthode stringSwitch2 en présupposant que la méthode createMHFromStrings2 existe.
  2. Ecrire le code de la méthode createMHFromStrings2 sachant qu'il faut créer un guardWithTest pour chaque arguments et vérifier que les tests passent.
    Note: mettre la création des method handles intermédiaires dans un champ static final si c'est possible !
  3. Le code précédent marche mais on peut noter que l'ordre dans lequel on fait les tests influs sur les performances. Au lieu de choisir un ordre arbitraire il existe une technique nommée inlining cache qui permet de créer l'arbre au fur et à mesure que le code est appelé avec des Strings différentes.
    On se propose de fournir une nouvelle implantation, nommée stringSwitch3 (ainsi qu'une méthode createMHFromStrings3) qui utilise un MutableCallSite pour implanter un inlining cache.
    L'idée est à chaque fois que l'on appel stringSwitch3
    • si la string n'a jamais été vu, la méthode slowPath appelle stringSwitch (la première version que l'on a écrite) et on ajoute dynamiquement un guardWithTest pour que la prochaine fois l'appel ne passe plus dans slowPath mais renvoie la valeur directement en passant dans le guardWithTest.
    • si la string a déjà été vu, on renvoie la même valeur que la fois précédente (d'où la notion de cache).

    Voici un squelette du code
      public static MethodHandle createMHFromStrings3(String... matches) {
        return new InliningCache(matches).dynamicInvoker(); 
      }
      
      static class InliningCache extends MutableCallSite {
        private static final MethodHandle SLOW_PATH;
        static {
          SLOW_PATH = ...
        }
        
        private final List<String> matches;
        
        public InliningCache(String... matches) {
          super(MethodType.methodType(int.class, String.class));
          this.matches = List.of(matches);
          setTarget(MethodHandles.insertArguments(SLOW_PATH, 0, this));
        }
        
        private int slowPath(String value) {
          // TODO
        }
      }
        
    Dans un premier temps, faite en sorte que les tests utilisent aussi stringSwitch3.
  4. Ecrire le code de slowPath et vérifier que les tests passent.
  5. On peut remarquer qu'il existe deux façon d'ajouter un nouveau guardWithTest, soit on peut le mettre devant soit devant soit derrière les autres guardWithTest existants.
    Sachant que statistiquement la première chaine de caractère que l'on demande est celle qui est le plus demandée, quelle est la meilleure implantation ?
    Modifier votre implanation en conséquence
  6. Ecrire un test JMH comparant les vitesses des méthodes stringSwitch, stringSwitch2 et stringSwitch3 avec un million de chaines de caractère ayant 6 valeurs différentes dont 3 des valeurs sont "foo", "bar" et "baz".