type - lambda stream java
¿Por qué se invocan las lambdas de Java 8 usando invokedynamic? (3)
La instrucción
invokedynamic
se utiliza para ayudar a la VM a determinar la referencia del método en tiempo de ejecución en lugar de cablearla en tiempo de compilación.
Esto es útil con lenguajes dinámicos donde el método exacto y los tipos de argumento no se conocen hasta el tiempo de ejecución.
Pero ese no es el caso con las lambdas de Java.
Se traducen a un método estático con argumentos bien definidos.
Y este método se puede invocar usando
invokestatic
.
Entonces, ¿cuál es la necesidad de
invokedynamic
para lambdas, especialmente cuando hay un golpe de rendimiento?
Brain Goetz explicó las razones de la estrategia de traducción lambda en uno de sus documentos que, lamentablemente, ahora no parecen estar disponibles. Afortunadamente guardé una copia:
Estrategia de traducción
Hay varias formas en que podríamos representar una expresión lambda en bytecode, como clases internas, identificadores de métodos, proxys dinámicos y otros. Cada uno de estos enfoques tiene pros y contras. Al seleccionar una estrategia, hay dos objetivos en competencia: maximizar la flexibilidad para la optimización futura al no comprometerse con una estrategia específica, frente a proporcionar estabilidad en la representación del archivo de clase. Podemos lograr ambos objetivos mediante el uso de la función dinámica invocada de JSR 292 para separar la representación binaria de la creación lambda en el código de bytes de la mecánica de evaluación de la expresión lambda en tiempo de ejecución. En lugar de generar bytecode para crear el objeto que implementa la expresión lambda (como llamar a un constructor para una clase interna), describimos una receta para construir el lambda y delegamos la construcción real al tiempo de ejecución del lenguaje. Esa receta está codificada en las listas de argumentos estáticos y dinámicos de una instrucción invocada dinámica.
El uso de invocados dinámicos nos permite diferir la selección de una estrategia de traducción hasta el tiempo de ejecución. La implementación en tiempo de ejecución es libre de seleccionar una estrategia dinámicamente para evaluar la expresión lambda. La opción de implementación en tiempo de ejecución está oculta detrás de una API estandarizada (es decir, parte de la especificación de la plataforma) para la construcción lambda, de modo que el compilador estático pueda emitir llamadas a esta API, y las implementaciones de JRE pueden elegir su estrategia de implementación preferida. La mecánica dinámica invocada permite que esto se realice sin los costos de rendimiento que este enfoque de enlace tardío podría imponer de otra manera.
Cuando el compilador encuentra una expresión lambda, primero baja (desugará) el cuerpo lambda a un método cuya lista de argumentos y tipo de retorno coincidan con los de la expresión lambda, posiblemente con algunos argumentos adicionales (para valores capturados del ámbito léxico, si los hay). ) En el punto en el que se capturaría la expresión lambda, genera un sitio de llamada dinámico invocado, que, cuando se invoca, devuelve una instancia de la interfaz funcional a la que se está convirtiendo el lambda. Este sitio de llamada se llama fábrica de lambda para una determinada lambda. Los argumentos dinámicos para la fábrica lambda son los valores capturados del ámbito léxico. El método bootstrap de la fábrica lambda es un método estandarizado en la biblioteca de tiempo de ejecución del lenguaje Java, llamado metafactory lambda. Los argumentos de arranque estáticos capturan información conocida sobre el lambda en tiempo de compilación (la interfaz funcional a la que se convertirá, un identificador de método para el cuerpo lambda desugared, información sobre si el tipo SAM es serializable, etc.)
Las referencias de método se tratan de la misma manera que las expresiones lambda, excepto que la mayoría de las referencias de método no necesitan ser eliminadas en un nuevo método; simplemente podemos cargar un identificador de método constante para el método referenciado y pasarlo a la metafactory.
Entonces, la idea aquí parecía ser encapsular la estrategia de traducción y no comprometerse a una forma particular de hacer las cosas ocultando esos detalles. En el futuro, cuando se hayan resuelto los tipos de borrado y falta de valor, y tal vez Java admita tipos de funciones reales, podrían ir allí y cambiar esa estrategia por otra sin causar ningún problema en el código de los usuarios.
La implementación lambda actual de Java 8 es una decisión compuesta:
-
- Compile la expresión lambda al método estático en la clase adjunta; en lugar de compilar lambdas para separar archivos de clase interna (Scala compila de esta manera, por lo que hay muchos archivos de clase $$$)
-
- Introduzca un grupo constante: BootstrapMethods, que ajusta la invocación del método estático al objeto del sitio de llamadas (se puede almacenar en caché para su uso posterior)
Entonces para responder tu pregunta,
-
- La implementación actual de lambda usando invokedynamic es un poco más rápida que la forma de clase interna separada, porque no es necesario cargar estos archivos de clase interna, sino crear el byte de clase interna [] sobre la marcha (para satisfacer, por ejemplo, la interfaz de función), y en caché para su uso posterior.
-
- El equipo de JVM aún puede optar por generar archivos de clase interna separados (por referencia a los métodos estáticos de la clase adjunta), es flexible
Las lambdas no se invocan usando invocacional
invokedynamic
, su representación de objeto se crea usando
invokedynamic
, la invocación real es una invocación
invokevirtual
o
invokeinterface
invocación regular.
Por ejemplo:
// 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");
}
Cualquier lambda tiene que convertirse en una instancia de alguna clase base o interfaz. Esa instancia a veces contendrá una copia de las variables capturadas del método original y, a veces, un puntero al objeto principal. Esto se puede implementar como una clase anónima.
Por qué invocar dinámicamente
La respuesta corta es: generar código en tiempo de ejecución.
Los mantenedores de Java optaron por generar la clase de implementación en tiempo de ejecución.
Esto se hace llamando a
java.lang.invoke.LambdaMetafactory.metafactory
.
Dado que los argumentos para esa llamada (tipo de retorno, interfaz y parámetros capturados) pueden cambiar, esto requiere
invokedynamic
.
El uso de
invokedynamic
para construir la clase anónima en tiempo de ejecución permite que la JVM genere ese
invokedynamic
de
invokedynamic
de clase en tiempo de ejecución.
Las llamadas posteriores a la misma declaración utilizan una versión en caché.
La otra razón para usar
invokedynamic
es poder cambiar la estrategia de implementación en el futuro sin tener que cambiar el código ya compilado.
El camino no tomado
La otra opción sería el compilador creando una clase interna para cada instancia lambda, equivalente a traducir el código anterior a:
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);
}
);
Esto requiere crear clases en tiempo de compilación y tener que cargarlas durante el tiempo de ejecución. La forma en que jvm funciona esas clases residiría en el mismo directorio que la clase original. Y la primera vez que ejecuta la instrucción que usa esa lambda, esa clase anónima tendría que cargarse e inicializarse.
Sobre el rendimiento
La primera llamada a
invokedynamic
activará la generación de clase anónima.
Luego, el código de operación
invokedynamic
se reemplaza con un
code
que es equivalente en rendimiento a la escritura manual de la instancia anónima.