Considere que tengo un código como el siguiente:
class Foo {
Y func(X x) {...}
void doSomethingWithAFunc(Function<X,Y> f){...}
void hotFunction(){
doSomethingWithAFunc(this::func);
}
}
Supongamos que hotFunction
se llama muy a menudo. Entonces, ¿sería aconsejable almacenar en caché this::func
, tal vez así:
class Foo {
Function<X,Y> f = this::func;
...
void hotFunction(){
doSomethingWithAFunc(f);
}
}
En lo que respecta a mi comprensión de las referencias al método Java, la máquina virtual crea un objeto de una clase anónima cuando se utiliza una referencia al método. Por lo tanto, almacenar en caché la referencia crearía ese objeto solo una vez, mientras que el primer enfoque lo crea en cada llamada de función. ¿Es esto correcto?
¿Deben almacenarse en caché las referencias a métodos que aparecen en posiciones activas en el código o la máquina virtual puede optimizar esto y hacer que el almacenamiento en caché sea superfluo? ¿Existe una práctica recomendada general al respecto o esta implementación de VM es muy específica, ya sea que dicho almacenamiento en caché sea de alguna utilidad?
fuente
invokedynamic
. Dudo que vea una mejora en el rendimiento almacenando en caché el objeto de función. Al contrario: podría inhibir las optimizaciones del compilador. ¿Comparaste el rendimiento de las dos variantes?invokedynamic
exige que se deba usar? ¡No veo ninguna razón para esto aquí!Respuestas:
Debe hacer una distinción entre ejecuciones frecuentes del mismo sitio de llamada , para lambda sin estado o lambdas con estado, y usos frecuentes de una referencia de método al mismo método (por diferentes sitios de llamada).
Mira los siguientes ejemplos:
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"); }
Aquí, el mismo sitio de llamada se ejecuta dos veces, produciendo una lambda sin estado y se imprimirá la implementación actual
"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"); } }
En este segundo ejemplo, el mismo sitio de llamada se ejecuta dos veces, produciendo una lambda que contiene una referencia a un
Runtime
instancia y la implementación actual imprimirá"unshared"
pero"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");
Por el contrario, en el último ejemplo hay dos sitios de llamadas diferentes que producen una referencia de método equivalente, pero a partir de
1.8.0_05
este se imprimirán"unshared"
y"unshared class"
.Para cada expresión lambda o referencia de método, el compilador emitirá una
invokedynamic
instrucción que se refiere a un método bootstrap proporcionado por JRE en la claseLambdaMetafactory
y los argumentos estáticos necesarios para producir la clase de implementación lambda deseada. Se deja al JRE real lo que produce la metafábrica, pero es un comportamiento específico de lainvokedynamic
instrucción recordar y reutilizar laCallSite
instancia creada en la primera invocación.El JRE actual produce un
ConstantCallSite
contiene unaMethodHandle
a una constante para lambdas sin estado (y no hay ninguna razón imaginable para hacerlo de manera diferente). Y referencias de métodos astatic
método método siempre son apátridas. Entonces, para lambdas sin estado y sitios de llamada única, la respuesta debe ser: no almacenar en caché, la JVM funcionará y, si no lo hace, debe tener fuertes razones por las que no debe contrarrestar.Para lambdas que tienen parámetros, y
this::func
es una lambda que tiene una referencia a lathis
instancia, las cosas son un poco diferentes. El JRE puede almacenarlos en caché, pero esto implicaría mantener algún tipo de valorMap
entre los valores reales de los parámetros y la lambda resultante, lo que podría ser más costoso que simplemente crear de nuevo esa instancia lambda estructurada simple. El JRE actual no almacena en caché las instancias lambda que tienen un estado.Pero esto no significa que la clase lambda se cree siempre. Simplemente significa que el sitio de llamada resuelto se comportará como una construcción de objeto ordinaria que instancia la clase lambda que se ha generado en la primera invocación.
Se aplican cosas similares a las referencias de métodos al mismo método de destino creado por diferentes sitios de llamada. El JRE puede compartir una única instancia lambda entre ellos, pero en la versión actual no lo hace, probablemente porque no está claro si el mantenimiento de la caché dará sus frutos. Aquí, incluso las clases generadas pueden diferir.
Por lo tanto, el almacenamiento en caché como en su ejemplo podría hacer que su programa haga cosas diferentes que sin él. Pero no necesariamente más eficiente. Un objeto en caché no siempre es más eficiente que un objeto temporal. A menos que realmente mida un impacto en el rendimiento causado por una creación de lambda, no debe agregar ningún almacenamiento en caché.
Creo que solo hay algunos casos especiales en los que el almacenamiento en caché podría ser útil:
fuente
invokedynamic
instrucción que creará el lambda. No es el lugar donde se ejecutará el método de interfaz funcional.this
-capturar lambdas eran singletons de ámbito de instancia (una variable de instancia sintética en el objeto). ¿No es así?1.8.0_40
no es así. Estas lambdas no se recuerdan, por lo que se pueden recolectar como basura. Pero recuerde que una vez queinvokedynamic
se ha vinculado un sitio de llamadas, puede optimizarse como el código normal, es decir, el Análisis de escape funciona para tales instancias lambda.MethodReference
. ¿Quieres decirMethodHandle
aquí?Una situación en la que es un buen ideal, desafortunadamente, es si la lambda se pasa como un oyente que desea eliminar en algún momento en el futuro. Se necesitará la referencia en caché, ya que pasar otra referencia de este :: método no se verá como el mismo objeto en la eliminación y el original no se eliminará. Por ejemplo:
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); } } }
Hubiera sido bueno no necesitar lambdaRef en este caso.
fuente
Por lo que entiendo la especificación del lenguaje, permite este tipo de optimización incluso si cambia el comportamiento observable. Consulte las siguientes citas de la sección JSL8 §15.13.3 :
Una prueba simple muestra que las referencias de métodos para métodos estáticos (pueden) dar como resultado la misma referencia para cada evaluación. El siguiente programa imprime tres líneas, de las cuales las dos primeras son idénticas:
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); } }
No puedo reproducir el mismo efecto para funciones no estáticas. Sin embargo, no he encontrado nada en la especificación del lenguaje que inhiba esta optimización.
Por lo tanto, mientras no haya un análisis de rendimiento para determinar el valor de esta optimización manual, lo desaconsejo. El almacenamiento en caché afecta la legibilidad del código y no está claro si tiene algún valor. La optimización prematura es la fuente de todos los males.
fuente