web-dev-qa-db-fra.com

Pourquoi Java 8 lambdas sont invoqués en utilisant invokedynamic?

L'instruction invokedynamic est utilisée pour aider le VM à déterminer la référence de la méthode au moment de l'exécution au lieu de la câbler au moment de la compilation.

Ceci est utile avec les langages dynamiques où la méthode exacte et les types d'arguments ne sont pas connus avant l'exécution. Mais ce n'est pas le cas avec Java lambdas. Ils sont traduits en une méthode statique avec des arguments bien définis. Et cette méthode peut être invoquée en utilisant invokestatic.

Alors, quel est le besoin de invokedynamic pour les lambdas, surtout quand il y a un impact sur les performances?

62
Kshitiz Sharma

Les lambdas ne sont pas invoquées à l'aide de invokedynamic, leur représentation d'objet est créée à l'aide de invokedynamic, l'invocation réelle est un invokevirtual ou invokeinterface régulier.

Par exemple:

// creates an instance of (a subclass of) Consumer 
// with invokedynamic to Java.lang.invoke.LambdaMetafactory 
something(x -> System.out.println(x));   

void something(Consumer<String> consumer) {
      // invokeinterface
      consumer.accept("hello"); 
}

Tout lambda doit devenir une instance d'une classe ou d'une interface de base. Cette instance contiendra parfois une copie des variables capturées à partir de la méthode d'origine et parfois un pointeur vers l'objet parent. Cela peut être implémenté en tant que classe anonyme.

Pourquoi invokedynamic

La réponse courte est: pour générer du code lors de l'exécution.

Les responsables Java ont choisi de générer la classe d'implémentation lors de l'exécution. Cela se fait en appelant Java.lang.invoke.LambdaMetafactory.metafactory. Étant donné que les arguments de cet appel (type de retour, interface et paramètres capturés) peuvent changer, cela nécessite invokedynamic.

L'utilisation de invokedynamic pour construire la classe anonyme lors de l'exécution permet à la machine virtuelle Java de générer ce bytecode de classe lors de l'exécution. Les appels suivants à la même instruction utilisent une version mise en cache. L'autre raison d'utiliser invokedynamic est de pouvoir changer la stratégie d'implémentation à l'avenir sans avoir à changer le code déjà compilé.

La route non empruntée

L'autre option serait que le compilateur crée une classe interne pour chaque instanciation lambda, équivalant à traduire le code ci-dessus en:

something(new Consumer() { 
    public void accept(x) {
       // call to a generated method in the base class
       ImplementingClass.this.lambda$1(x);

       // or repeating the code (awful as it would require generating accesors):
       System.out.println(x);
    }
);   

Cela nécessite la création de classes au moment de la compilation et le chargement lors de l'exécution. La façon dont jvm fonctionne ces classes résiderait dans le même répertoire que la classe d'origine. Et la première fois que vous exécutez l'instruction qui utilise ce lambda, cette classe anonyme devrait être chargée et initialisée.

À propos des performances

Le premier appel à invokedynamic déclenchera la génération de classe anonyme. Ensuite, l'opcode invokedynamic est remplacé par code dont les performances sont équivalentes à l'écriture manuelle de l'instanciation anonyme.

67
Daniel Sperry

Brain Goetz a expliqué les raisons de la stratégie de traduction lambda dans l'un de ses articles qui malheureusement ne semblent plus disponibles. Heureusement j'en ai gardé une copie:

Stratégie de traduction

Il existe plusieurs façons de représenter une expression lambda dans le bytecode, telles que les classes internes, les descripteurs de méthode, les proxys dynamiques et autres. Chacune de ces approches a ses avantages et ses inconvénients. Dans la sélection d'une stratégie, il y a deux objectifs concurrents: maximiser la flexibilité pour une optimisation future en ne s'engageant pas dans une stratégie spécifique, vs assurer la stabilité dans la représentation du fichier de classe. Nous pouvons atteindre ces deux objectifs en utilisant la fonctionnalité dynamique invoquée de JSR 292 pour séparer la représentation binaire de la création lambda dans le bytecode de la mécanique d'évaluation de l'expression lambda au moment de l'exécution. Au lieu de générer du bytecode pour créer l'objet qui implémente l'expression lambda (comme appeler un constructeur pour une classe interne), nous décrivons une recette pour construire le lambda et déléguons la construction réelle au runtime du langage. Cette recette est codée dans les listes d'arguments statiques et dynamiques d'une instruction dynamique invoquée.

L'utilisation de invokedynamic nous permet de différer la sélection d'une stratégie de traduction jusqu'à l'exécution. L'implémentation d'exécution est libre de sélectionner une stratégie dynamiquement pour évaluer l'expression lambda. Le choix d'implémentation d'exécution est caché derrière une API standardisée (c'est-à-dire faisant partie des spécifications de la plate-forme) pour la construction lambda, de sorte que le compilateur statique puisse émettre des appels vers cette API, et les implémentations JRE peuvent choisir leur stratégie d'implémentation préférée. La mécanique dynamique invoquée permet de le faire sans les coûts de performance que cette approche de liaison tardive pourrait autrement imposer.

Lorsque le compilateur rencontre une expression lambda, il abaisse d'abord (desugars) le corps lambda dans une méthode dont la liste d'arguments et le type de retour correspondent à ceux de l'expression lambda, éventuellement avec quelques arguments supplémentaires (pour les valeurs capturées à partir de la portée lexicale, le cas échéant). ) Au point où l'expression lambda serait capturée, elle génère un site d'appel dynamique invoqué qui, une fois invoqué, renvoie une instance de l'interface fonctionnelle vers laquelle le lambda est converti. Ce site d'appel s'appelle l'usine lambda pour un lambda donné. Les arguments dynamiques de la fabrique lambda sont les valeurs capturées à partir de la portée lexicale. La méthode bootstrap de la fabrique lambda est une méthode standardisée dans la bibliothèque d'exécution du langage Java, appelée méta-usine lambda. Les arguments statiques bootstrap capturent les informations connues sur le lambda au moment de la compilation (l'interface fonctionnelle dans laquelle il sera converti, un descripteur de méthode pour le corps lambda désucré, des informations sur la sérialisation du type SAM, etc.). )

Les références de méthode sont traitées de la même manière que les expressions lambda, sauf que la plupart des références de méthode n'ont pas besoin d'être supprimées dans une nouvelle méthode; nous pouvons simplement charger un descripteur de méthode constant pour la méthode référencée et le transmettre à la métafabrique.

Donc, l'idée ici semblait être d'encapsuler la stratégie de traduction et de ne pas s'engager dans une façon particulière de faire les choses en cachant ces détails. À l'avenir, lorsque l'effacement des types et le manque de types de valeurs ont été résolus et que Java prend en charge les types de fonctions réels, ils pourraient tout aussi bien y aller et changer cette stratégie pour une autre sans causer de problèmes aux utilisateurs ' code.

43
Edwin Dalorzo

Java 8 est une décision composée:

    1. Compilez l'expression lambda en méthode statique dans la classe englobante; au lieu de compiler des lambdas pour séparer les fichiers de classe interne (Scala compile de cette façon, donc beaucoup de fichiers de classe $$$ autour)
    1. Introduire un pool constant: BootstrapMethods, qui encapsule l'appel de méthode statique à l'objet callsite (peut être mis en cache pour une utilisation ultérieure)

Donc, pour répondre à votre question,

    1. l'implémentation lambda actuelle utilisant invokedynamic est un peu plus rapide que la méthode de classe interne séparée, car il n'est pas nécessaire de charger ces fichiers de classe interne, mais de créer à la place l'octet de classe interne [] (pour satisfaire par exemple l'interface Function), et mis en cache pour une utilisation ultérieure.
    1. L'équipe JVM peut toujours choisir de générer des fichiers de classe interne séparés (par référence aux méthodes statiques de la classe englobante), c'est flexible
5
fjolt