¿Por qué la expresión Lambda en las nuevas funciones de Java 8-13 se ejecuta de manera ineficiente?

Prefacio

Por que dijeLambda表达式运行效率低。

Primero prepare una lista:

List<Integer> list = new ArrayList<>();
for (int i = 0; i < 10000; i++) {
    list.add(i);
}

Primero use expresiones Lambda para recorrer esta lista:

long lambdaStart = System.currentTimeMillis();
list.forEach(i -> {
    // 不用做事情,循环就够了
});
long lambdaEnd = System.currentTimeMillis();
System.out.println("lambda循环运行毫秒数===" + (lambdaEnd - lambdaStart));

El tiempo de ejecución es de aproximadamente 110 ms

Recicle esta lista de la forma habitual:

long normalStart = System.currentTimeMillis();
for (int i = 0; i < list.size(); i++) {
    // 不用做事情,循环就够了
}
long normalEnd = System.currentTimeMillis();
System.out.println("普通循环运行毫秒数===" + (normalEnd - normalStart));

El tiempo de ejecución es de aproximadamente 0 ms o 1 ms

Lo leyó bien, la diferencia en el tiempo de ejecución es tan grande, si no lo cree, puede probarlo usted mismo, y esto no es solo el uso de expresiones Lambda en el ciclo que conducirá a una baja eficiencia operativa, sino Lambda表达式在运行时就是会需要额外的时间que continuemos analizando.

análisis

Si queremos estudiar expresiones Lambda, la forma más correcta y directa es mirar las instrucciones de código de bytes a las que corresponde.

Utilice el siguiente comando para ver las instrucciones de código de bytes correspondientes al archivo de clase:

javap -v -p Test.class

Hay muchas instrucciones analizadas por el comando anterior. Aquí extraeré las partes más importantes para su análisis:

Las instrucciones de código de bytes correspondientes al uso de expresiones Lambda son las siguientes:

34: invokestatic  #6        // Method java/lang/System.currentTimeMillis:()J
37: lstore_2
38: aload_1
39: invokedynamic #7,  0    // InvokeDynamic #0:accept:()Ljava/util/function/Consumer;
44: invokeinterface #8,  2  // InterfaceMethod java/util/List.forEach:(Ljava/util/function/Consumer;)V
49: invokestatic  #6        // Method java/lang/System.currentTimeMillis:()J

Las instrucciones de código de bytes que no utilizan expresiones Lambda son las siguientes:

82: invokestatic  #6          // Method java/lang/System.currentTimeMillis:()J
85: lstore        6
87: iconst_0
88: istore        8
90: iload         8
92: aload_1
93: invokeinterface #17,  1   // InterfaceMethod java/util/List.size:()I
98: if_icmpge     107
101: iinc          8, 1
104: goto          90
107: invokestatic  #6         // Method java/lang/System.currentTimeMillis:()J

Se puede ver en las instrucciones de código de bytes correspondientes a los dos métodos anteriores que los métodos de ejecución de los dos métodos son realmente diferentes.

Proceso de ciclo

No utilice expresiones Lambda para ejecutar procesos cíclicos

Pasos de ejecución de la instrucción de bytecode:

82: invokestatic: ejecutar método estático, java / lang / System.currentTimeMillis :();
85-92: en pocas palabras, es para inicializar datos, int i = 0;
93: invokeinterface: ejecutar método de interfaz, la interfaz es List, por lo que se ejecuta Es el método ArrayList.size;
98: if_icmpge: comparación, equivalente a ejecutar i <list.size ();
101: iinc: i ++;
104: goto: ir al siguiente ciclo;
107: invokestatic: ejecutar un método estático;

Entonces, este proceso no debería ser un gran problema para todos, es una lógica de ciclo normal.

Uso de expresiones Lambda para ejecutar el proceso de bucle
Echemos un vistazo a las instrucciones de código de bytes correspondientes:

34: invokestatic  #6        // Method java/lang/System.currentTimeMillis:()J
37: lstore_2
38: aload_1
39: invokedynamic #7,  0    // InvokeDynamic #0:accept:()Ljava/util/function/Consumer;
44: invokeinterface #8,  2  // InterfaceMethod java/util/List.forEach:(Ljava/util/function/Consumer;)V
49: invokestatic  #6        // Method java/lang/System.currentTimeMillis:()J

Pasos de ejecución de la instrucción de bytecode:

  • 34: invokestatic: ejecuta un método estático, java / lang / System.currentTimeMillis :();
  • 37-38: inicializar datos
  • 39: invokedynamic: ¿Qué está haciendo esto?
  • 44: invokeinterface: ejecutar el método java / util / List.forEach ()
  • 49: invokestatic: ejecuta un método estático, java / lang / System.currentTimeMillis :();

No es lo mismo que la instrucción de código de bytes en el ciclo normal anterior. Echemos un vistazo más de cerca a esta instrucción de código de bytes. Este proceso no es como un proceso de ciclo, sino un proceso de ejecución secuencial de métodos:

  • Inicialice algunos datos primero
  • Ejecutar la instrucción dinámica invocada (qué hace esta instrucción temporalmente)
  • Luego ejecute el método java / util / List.forEach (), por lo que la lógica del bucle real está aquí

Entonces, podemos encontrar que cuando se usan bucles de expresión Lambda, se harán algunas otras cosas antes del bucle, por lo que el tiempo de ejecución será más largo.

Entonces, ¿qué hace exactamente la instrucción dinámica invocada?

El método java / util / List.forEach recibe un parámetro Consumer <? Super T> action, Consumer es una interfaz, así que si desea llamar a este método, debe pasar un objeto de ese tipo de interfaz.

Y en realidad pasamos una expresión Lambda en el código, por lo que podemos suponer aquí: la expresión Lambda debe convertirse en un objeto y el tipo de objeto debe invertirse en el tiempo de compilación de acuerdo con el lugar donde se usa la expresión Lambda. Empujar.

A continuación, se ofrece una explicación de la deducción inversa: una expresión Lambda puede ser utilizada por varios métodos, y el tipo de parámetro recibido por este método, es decir, la interfaz funcional, puede ser diferente, siempre que la interfaz funcional se ajuste a la expresión Lambda. La definición puede ser.

En este ejemplo, el compilador puede deducir en tiempo de compilación que la expresión Lambda corresponde a un objeto del tipo de interfaz Cosumer.

Entonces, si desea convertir una expresión Lambda en un objeto, necesita una clase que implemente la interfaz del consumidor.

Entonces, la pregunta ahora es ¿cuándo se generó esta clase y dónde se generó?

Entonces, lentamente deberíamos poder pensar,invokedynamic指令,它是不是就是先将Lambda表达式转换成某个类,然后生成一个实例以便提供给forEach方法调用呢?

Echemos un vistazo a la instrucción dinámica invocada:

invokedynamic #7,  0    // InvokeDynamic #0:accept:()Ljava/util/function/Consumer;

Hay cuatro instrucciones principales para llamar a funciones en Java: invokevirtual, invokespecial, invokestatic e invokeinterface. Se ha agregado una nueva instrucción invokeynamic a JSR 292. Esta instrucción representa la ejecución de un lenguaje dinámico, que es una expresión Lambda.

El # 0 en el comentario de la instrucción indica el método 0 en BootstrapMethods:

BootstrapMethods:
  0: #60 invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
    Method arguments:
      #61 (Ljava/lang/Object;)V
      #62 invokestatic com/luban/Test.lambda$main$0:(Ljava/lang/Integer;)V
      #63 (Ljava/lang/Integer;)V

Por lo tanto, cuando se ejecuta invokedynamic, realmente ejecuta los métodos en BootstrapMethods, como en este ejemplo: java / lang / invoke / LambdaMetafactory.metafactory.

el código se muestra a continuación:

public static CallSite metafactory(MethodHandles.Lookup caller,
                                       String invokedName,
                                       MethodType invokedType,
                                       MethodType samMethodType,
                                       MethodHandle implMethod,
                                       MethodType instantiatedMethodType)
            throws LambdaConversionException {
        AbstractValidatingLambdaMetafactory mf;
        mf = new InnerClassLambdaMetafactory(caller, invokedType,
                                             invokedName, samMethodType,
                                             implMethod, instantiatedMethodType,
                                             false, EMPTY_CLASS_ARRAY, EMPTY_MT_ARRAY);
        mf.validateMetafactoryArgs();
        return mf.buildCallSite();
    }

En este método se usa una clase particularmente obvia y fácil de entender: InnerClassLambdaMetafactory.

Esta clase es una clase de fábrica que genera clases internas para expresiones Lambda. Cuando se llama al método buildCallSite, se genera una clase interna y se genera una instancia de la clase.

Entonces, ahora para generar una clase interna, qué condiciones se necesitan:

Nombre de la clase: Se puede generar de acuerdo con algunas reglas
. La interfaz que la clase necesita implementar: se conoce en tiempo de compilación. En este caso, es la
interfaz del consumidor . El método en la interfaz de implementación: en este caso, es el método void accept (T t) de la interfaz del consumidor.

Entonces, ¿cómo debería implementar la clase interna el método void accept (T t)?

Echemos un vistazo al resultado de javap -v -p Test.class. Además de nuestra propia implementación, hay un método más:

private static void lambda$main$0(java.lang.Integer);
    descriptor: (Ljava/lang/Integer;)V
    flags: ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC
    Code:
      stack=0, locals=1, args_size=1
         0: return
      LineNumberTable:
        line 25: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       1     0     i   Ljava/lang/Integer;

Obviamente, este método lambda $ main $ 0 estático representa la expresión Lambda que escribimos, pero debido a que no hay lógica en la expresión Lambda en nuestro ejemplo, no hay nada en la parte de Código de esta instrucción de código de bytes.

Entonces, cuando ahora estamos implementando el método void accept (T t) en la clase interna, solo necesitamos llamar a este método estático lambda $ main $ 0.

Entonces, en este punto, una clase interna se puede implementar normalmente. Una vez que la clase interna está disponible, la expresión Lambda se puede convertir en un objeto de la clase interna y se puede realizar el ciclo.

No es fácil de crear y creo que es bueno. Comente sobre Sanlian

Supongo que te gusta

Origin blog.csdn.net/Javayinlei/article/details/109333115
Recomendado
Clasificación