Why does Lambda expression in Java 8-13 new features run inefficiently?

Preface

Why i saidLambda表达式运行效率低。

First prepare a list:

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

First use Lambda expressions to loop this list:

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

Running time is about 110ms

Recycle this list in the usual way:

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

Running time is about 0ms or 1ms

You read that right, the difference in running time is so big, if you don't believe it, you can try it yourself, and this is not only the use of Lambda expressions in the loop will lead to low operating efficiency, but Lambda表达式在运行时就是会需要额外的时间let us continue to analyze.

analysis

If we want to study Lambda expressions, the most correct and direct way is to look at the bytecode instructions it corresponds to.

Use the following command to view the bytecode instructions corresponding to the class file:

javap -v -p Test.class

There are a lot of instructions parsed by the above command. I will extract the more important parts for your analysis:

The bytecode instructions corresponding to the use of Lambda expressions are as follows:

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

The bytecode instructions that do not use Lambda expressions are as follows:

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

As can be seen from the bytecode instructions corresponding to the above two methods, the execution methods of the two methods are indeed different.

Cycle process

Do not use Lambda expressions to execute cyclic processes

Bytecode instruction execution steps:

82:invokestatic: execute static method, java/lang/System.currentTimeMillis:();
85-92: Simply put, it is to initialize data, int i = 0;
93:invokeinterface: execute interface method, interface is List, so it is executed It is the ArrayList.size method;
98:if_icmpge: comparison, equivalent to executing i <list.size();
101:iinc: i++;
104:goto: go to the next loop;
107:invokestatic: execute a static method;

So this process should not be a big problem for everyone, it is a normal loop logic.

Using Lambda expressions to execute the loop process
Let's take a look at the corresponding bytecode instructions:

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

Bytecode instruction execution steps:

  • 34: invokestatic: Execute static method, java/lang/System.currentTimeMillis:();
  • 37-38: Initialize data
  • 39: invokedynamic: What is this doing?
  • 44: invokeinterface: execute java/util/List.forEach() method
  • 49: invokestatic: execute a static method, java/lang/System.currentTimeMillis:();

It is not the same as the bytecode instruction in the normal loop above. Let's take a closer look at this bytecode instruction. This process is not like a loop process, but a process of sequential execution of methods:

  • Initialize some data first
  • Execute the invokedynamic instruction (what does this instruction do temporarily)
  • Then execute the java/util/List.forEach() method, so the real loop logic is here

So we can find that when using Lambda expression loop, some other things will be done before the loop, so the execution time will be longer.

So what exactly does the invokedynamic instruction do?

The java/util/List.forEach method receives a parameter Consumer<? super T> action, Consumer is an interface, so if you want to call this method, you must pass an object of that interface type.

And we actually pass a Lambda expression in the code, so we can assume here: the Lambda expression needs to be converted into an object, and the type of the object needs to be reversed at compile time according to the place where the Lambda expression is used. Push.

Here is an explanation of reverse deduction: a Lambda expression can be used by multiple methods, and the parameter type received by this method, that is, the functional interface, can be different, as long as the functional interface conforms to the Lambda expression The definition can be.

In this example, the compiler can deduced at compile time that the Lambda expression corresponds to an object of the Cosumer interface type.

So if you want to convert a Lambda expression into an object, you need a class that implements the Consumer interface.

So, the question now is when was this class generated and where was it generated?

So, slowly we should be able to think,invokedynamic指令,它是不是就是先将Lambda表达式转换成某个类,然后生成一个实例以便提供给forEach方法调用呢?

Let's look back at the invokedynamic instruction:

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

There are four major instructions for calling functions in Java: invokevirtual, invokespecial, invokestatic, and invokeinterface. A new instruction invokedynamic has been added to JSR 292. This instruction represents the execution of a dynamic language, which is a Lambda expression.

The #0 in the instruction comment indicates the 0th method in 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

So when invokedynamic is executed, it actually executes the methods in BootstrapMethods, such as in this example: java/lang/invoke/LambdaMetafactory.metafactory.

code show as below:

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();
    }

A particularly obvious and easy-to-understand class is used in this method: InnerClassLambdaMetafactory.

This class is a factory class that generates inner classes for Lambda expressions. When the buildCallSite method is called, an internal class is generated and an instance of the class is generated.

So now to generate an inner class, what conditions are needed:

Class name: It can be generated according to some rules
. The interface that the class needs to implement: it is known at compile time. In this case, it is the Consumer
interface. The method in the implementation interface: In this case, it is the void accept(T t) method of the Consumer interface.

So how should the inner class implement the void accept(T t) method?

Let's take a look at the result of javap -v -p Test.class. In addition to our own method, there is one more method:

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;

Obviously, this static lambda$main$0 method represents the Lambda expression we wrote, but because there is no logic in the Lambda expression in our example, there is nothing in the Code part of this bytecode instruction.

So, when we are now implementing the void accept(T t) method in the inner class, we only need to call this lambda$main$0 static method.

So at this point, an inner class can be implemented normally. After the inner class is available, the Lambda expression can be converted into an object of the inner class, and it can be looped.

It’s not easy to create, and I think it’s a good one. Please comment on Sanlian

Guess you like

Origin blog.csdn.net/Javayinlei/article/details/109333115