11 octobre 2012

Lambda, under the hood

Retour avec Brian Goetz, cette fois pour comprendre comment les lambda sont implémentées. Comme je n'ai pas pris de photos de cette session, j'agrémente ce biller de quelques images prises au hasard :)

L’introduction des lambdas dans Java aurait pu se faire en introduisant un type « Function » dans la structure du bytecode. Cependant, ce genre d’évolution et les impacts en termes de compatibilité nécessitent de longs travaux, aussi une modification dans le compilateur est largement préférée !



On pratique déjà plus ou moins les lambda dans le code java avant la version 8 via les « interfaces fonctionnelles » n’ayant qu’une seule méthode (Runnable, Comparable, …). Première idée donc, simplement demander au compilo de convertir chaque lambda en inner class anonyme. On se retrouverait alors avec une classe par lambda dans le code, autant dire que le nombre de classe par fichier source java exploserait rapidement, et surtout un appel de constructeur par appel de lambda, et là vos perfs s'écroulent.

Seconde idée, utiliser les mécanisme d’invocation du bytecode pour convertir la lambda en méthode statique et l’invoquer. Idée intéressante mais dont les impacts sont peu évidents sans plus de recul, et n’oublions pas que ce genre d’évolution devra rester dans toutes les versions à venir de Java, pas le droit à l’erreur ! (voir l’article précédent sur les génériques pour vous en convaincre…)

Java 7 a cependant ajouté un truc bien sympa, le invokeDynamic qui profite déjà à Grovvy 2.0. Ce mécanisme permet d’attendre l’appel de méhode pour identifier la cible à invoquer, passerelle vers l’exécution rapide des langages dynamiques sans payer le prix de l’API de réflexion. Un MethodHandle peut ainsi être utilisé pour pointer vers la méthode à exécuter. Détail important, ce "pointeur" peut changer à chaque invocation, mais en pratique est souvent fixe, et peut donc être caché pour obtenir des performances proches d’une invocation directe.

L’ajout de invokedynamic, au départ pour ouvrir la porte aux langages dynamiques, profite ainsi aux lambdas. La JRE Java 8 propose ainsi une factory pour identifier le MethodHandle, et le conserve en cache tant que les types des arguments ne changent pas, ce qui est le cas pour la majorité des cas d'usage de lambdas. Cette factory peut au choix générer du bytecode, utiliser des wrappers, des proxies, ou des API internes, les lambdas sont « neutres » au niveau bytecode et laissent la porte ouverte pour de futures nouvelles stratégies ou améliorations, ou bien s’adapter en fonction du contexte : JRE embarqué par exemple.



Tout serait merveilleux, sans l’un des mécanisme les plus poluants que Java ai pu inventer : la sérialization.

Si ma JVM a généré du code, que je le sérialise, et qu’une autre JVM, basée sur une autre stratégie tente de le désérialiser, que va t-il se passer :-/ ? D’où … une factory pour la serialisation, dont le but est de capturer la « recette » de la lambda pour être capable de la reconstruire sur la JVM cible. Ce qui veut dire qu’il faut prévoir qu’un vilain programmeur pourrait construire à la volée un flux sérialisé comportant du code malicieux, et l’injecter par ce biais dans votre JVM, ce qui ne serait vraiment pas gentil …. Hum, ça donne envie, n’est-ce pas ?

Au final, une lambda est, en termes de performances, au pire équivalente à une inner classe, et dans de nombreux cas (pas de capture de variables du contexte) 5 à 20 fois plus rapide. Bref, pour une fois on pourrait bien avoir une évolution bien foutue dans Java !