web-dev-qa-db-fra.com

La mise en cache des références de méthode est-elle une bonne idée dans Java 8?

Considérez que j'ai du code comme celui-ci:

class Foo {

   Y func(X x) {...} 

   void doSomethingWithAFunc(Function<X,Y> f){...}

   void hotFunction(){
        doSomethingWithAFunc(this::func);
   }

}

Supposons que hotFunction soit appelé très souvent. Serait-il alors conseillé de mettre en cache this::func, peut-être comme ceci:

class Foo {
     Function<X,Y> f = this::func;
     ...
     void hotFunction(){
        doSomethingWithAFunc(f);
     }
}

En ce qui concerne ma compréhension de Java vont, la machine virtuelle crée un objet d'une classe anonyme lorsqu'une référence de méthode est utilisée. Ainsi, la mise en cache de la référence ne créerait cet objet qu'une seule fois tandis que le la première approche le crée à chaque appel de fonction. Est-ce correct?

Les références de méthode qui apparaissent à des positions chaudes dans le code doivent-elles être mises en cache ou le VM est-il capable d'optimiser cela et de rendre la mise en cache superflue? Existe-t-il une meilleure pratique générale à ce sujet ou est-ce fortement VM- mise en œuvre spécifique si une telle mise en cache est utile?

71
gexicide

Vous devez faire une distinction entre les exécutions fréquentes du même site d'appel , pour les lambda sans état ou les lambdas avec état, et les utilisations fréquentes d'un référence de méthode à la même méthode (par différents sites d'appel).

Regardez les exemples suivants:

    Runnable r1=null;
    for(int i=0; i<2; i++) {
        Runnable r2=System::gc;
        if(r1==null) r1=r2;
        else System.out.println(r1==r2? "shared": "unshared");
    }

Ici, le même site d'appel est exécuté deux fois, produisant un lambda sans état et l'implémentation actuelle affichera "shared".

Runnable r1=null;
for(int i=0; i<2; i++) {
  Runnable r2=Runtime.getRuntime()::gc;
  if(r1==null) r1=r2;
  else {
    System.out.println(r1==r2? "shared": "unshared");
    System.out.println(
        r1.getClass()==r2.getClass()? "shared class": "unshared class");
  }
}

Dans ce deuxième exemple, le même site d'appel est exécuté deux fois, produisant un lambda contenant une référence à une instance Runtime et l'implémentation actuelle affichera "unshared" mais "shared class".

Runnable r1=System::gc, r2=System::gc;
System.out.println(r1==r2? "shared": "unshared");
System.out.println(
    r1.getClass()==r2.getClass()? "shared class": "unshared class");

En revanche, dans le dernier exemple, deux sites d'appel différents produisent une référence de méthode équivalente mais en date de 1.8.0_05 il imprimera "unshared" et "unshared class".


Pour chaque expression lambda ou référence de méthode, le compilateur émet une instruction invokedynamic qui fait référence à un JRE fourni bootstrap dans la classe LambdaMetafactory et les arguments statiques nécessaires pour produire la classe d'implémentation lambda souhaitée. Il est laissé au JRE réel ce que la méta-usine produit mais c'est un comportement spécifié de l'instruction invokedynamic de se souvenir et de réutiliser le CallSite instance créée lors de la première invocation.

Le JRE actuel produit un ConstantCallSite contenant un MethodHandle vers un objet constant pour les lambdas sans état (et il n'y a aucune raison imaginable de le faire différemment) ). Et les références de méthode à la méthode static sont toujours sans état. Donc, pour les lambdas apatrides et les sites d'appel uniques, la réponse doit être: ne pas mettre en cache, la JVM le fera et si ce n'est pas le cas, elle doit avoir de bonnes raisons de ne pas contre-attaquer.

Pour les lambdas ayant des paramètres, et this::func est un lambda qui a une référence à l'instance this, les choses sont un peu différentes. Le JRE est autorisé à les mettre en cache, mais cela impliquerait de maintenir une sorte de Map entre les valeurs réelles des paramètres et le lambda résultant, ce qui pourrait être plus coûteux que de simplement recréer cette instance lambda structurée simple. Le JRE actuel ne met pas en cache les instances lambda ayant un état.

Mais cela ne signifie pas que la classe lambda est créée à chaque fois. Cela signifie simplement que le site d'appel résolu se comportera comme une construction d'objet ordinaire instanciant la classe lambda qui a été générée lors de la première invocation.

Des choses similaires s'appliquent aux références de méthode à la même méthode cible créée par différents sites d'appel. Le JRE est autorisé à partager une seule instance lambda entre eux, mais dans la version actuelle, ce n'est pas le cas, probablement parce qu'il n'est pas clair si la maintenance du cache sera payante. Ici, même les classes générées peuvent différer.


Ainsi, la mise en cache comme dans votre exemple pourrait faire en sorte que votre programme fasse des choses différentes de celles sans. Mais pas forcément plus efficace. Un objet mis en cache n'est pas toujours plus efficace qu'un objet temporaire. À moins que vous ne mesuriez vraiment un impact sur les performances provoqué par une création lambda, vous ne devez pas ajouter de cache.

Je pense qu'il n'y a que quelques cas particuliers où la mise en cache peut être utile:

  • nous parlons de beaucoup de sites d'appel différents faisant référence à la même méthode
  • le lambda est créé dans le constructeur/classe initialize parce que plus tard le site d'utilisation
    • être appelé par plusieurs threads simultanément
    • souffrir de la baisse des performances de la première invocation
71
Holger

Pour autant que je comprenne la spécification du langage, il permet ce type d'optimisation même s'il modifie le comportement observable. Voir les citations suivantes de la section JSL8 §15.13. :

§15.13.3 Évaluation au moment de l'exécution des références de méthode

Au moment de l'exécution, l'évaluation d'une expression de référence de méthode est similaire à l'évaluation d'une expression de création d'instance de classe, dans la mesure où l'achèvement normal produit une référence à un objet. [..]

[..] Soit une nouvelle instance d'une classe avec les propriétés ci-dessous est allouée et initialisée, soit une existante l'instance d'une classe avec les propriétés ci-dessous est référencée.

Un test simple montre que les références de méthode pour les méthodes statiques (peuvent) donner la même référence pour chaque évaluation. Le programme suivant imprime trois lignes, dont les deux premières sont identiques:

public class Demo {
    public static void main(String... args) {
        foobar();
        foobar();
        System.out.println((Runnable) Demo::foobar);
    }
    public static void foobar() {
        System.out.println((Runnable) Demo::foobar);
    }
}

Je ne peux pas reproduire le même effet pour les fonctions non statiques. Cependant, je n'ai rien trouvé dans la spécification du langage qui empêche cette optimisation.

Donc, tant qu'il n'y a pas analyse de performance pour déterminer la valeur de cette optimisation manuelle, je le déconseille fortement. La mise en cache affecte la lisibilité du code, et il n'est pas clair s'il a une valeur. L'optimisation prématurée est la racine de tout mal.

7
nosid

Une situation où c'est un bon idéal, malheureusement, est si le lambda est passé en tant qu'auditeur que vous souhaitez supprimer à un moment donné dans le futur. La référence mise en cache sera nécessaire car en passant une autre référence this :: method ne sera pas considérée comme le même objet dans la suppression et l'original ne sera pas supprimé. Par exemple:

public class Example
{
    public void main( String[] args )
    {
        new SingleChangeListenerFail().listenForASingleChange();
        SingleChangeListenerFail.observableValue.set( "Here be a change." );
        SingleChangeListenerFail.observableValue.set( "Here be another change that you probably don't want." );

        new SingleChangeListenerCorrect().listenForASingleChange();
        SingleChangeListenerCorrect.observableValue.set( "Here be a change." );
        SingleChangeListenerCorrect.observableValue.set( "Here be another change but you'll never know." );
    }

    static class SingleChangeListenerFail
    {
        static SimpleStringProperty observableValue = new SimpleStringProperty();

        public void listenForASingleChange()
        {
            observableValue.addListener(this::changed);
        }

        private<T> void changed( ObservableValue<? extends T> observable, T oldValue, T newValue )
        {
            System.out.println( "New Value: " + newValue );
            observableValue.removeListener(this::changed);
        }
    }

    static class SingleChangeListenerCorrect
    {
        static SimpleStringProperty observableValue = new SimpleStringProperty();
        ChangeListener<String> lambdaRef = this::changed;

        public void listenForASingleChange()
        {
            observableValue.addListener(lambdaRef);
        }

        private<T> void changed( ObservableValue<? extends T> observable, T oldValue, T newValue )
        {
            System.out.println( "New Value: " + newValue );
            observableValue.removeListener(lambdaRef);
        }
    }
}

Cela aurait été bien de ne pas avoir besoin de lambdaRef dans ce cas.

7
user2219808