zoukankan      html  css  js  c++  java
  • java8新特性Lambda表达式为什么运行效率低

    Lambda表达式为什么运行效率低

    准备

    我为什么说Lambda表达式运行效率低

    先准备一个list:

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

    先用Lambda表达式的方式来循环一下这个list:

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

    运行时间大概为110ms

    再用普通方式来循环一下这个list:

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

    运行时间大概为0ms或1ms

    你们没看错,运行时间差别就是这么大,不相信的话大家可以自己去试一下,并且这并不是只有在循环时使用Lambda表达式才会导致运行效率低,而是Lambda表达式在运行时就是会需要额外的时间,我们继续来分析。

    分析

    如果我们要研究Lambda表达式,最正确、最直接的方法就是查看它所对应的字节码指令。

    使用以下命令查看class文件对应的字节码指令:

    javap -v -p Test.class

    上述命令解析出来的指令非常多,我这里提取比较重要的部分来给大家分析:

    使用Lambda表达式所对应的字节码指令如下:

    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

    不使用Lambda表达式所对应的字节码指令如下:

    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

    从上面两种方式所对应的字节码指令可以看出,两种方式的执行方式确实不太一样。

    不使用Lambda表达式执行循环流程

    字节码指令执行步骤:

    • 82:invokestatic:     执行静态方法,java/lang/System.currentTimeMillis:();
    • 85-92:                      简单来说就是初始化数据,int i = 0;
    • 93:invokeinterface:执行接口方法,接口为List,所以真正执行的是就是ArrayList.size方法;
    • 98:if_icmpge:         比较,相当于执行i < list.size();
    • 101:iinc:                 i++;
    • 104:goto:               进行下一次循环;
    • 107:invokestatic:   执行静态方法;

    那么这个流程大家应该问题不大,是一个很正常的循环逻辑。

    使用Lambda表达式执行循环流程

    我们再来看一下对应的字节码指令:

    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

    字节码指令执行步骤:

    • 34: invokestatic:        执行静态方法,java/lang/System.currentTimeMillis:();
    • 37-38:                        初始化数据
    • 39: invokedynamic:   这是在干什么?
    • 44: invokeinterface:   执行java/util/List.forEach()方法
    • 49: invokestatic:        执行静态方法,java/lang/System.currentTimeMillis:();

    和上面正常循环的方式的字节码指令不太一样,我们认真的看一下这个字节码指令,这个流程并不像是一个循环的流程,而是一个方法顺序执行的流程:

    • 先初始化一些数据
    • 执行invokedynamic指令(暂时这个指令是做什么的
    • 然后执行java/util/List.forEach()方法,所以真正的循环逻辑在这里

    所以我们可以发现,使用Lambda表达式循环时,在循环前会做一些其他事情,所以导致执行时间要更长一点。

     

    那么invokedynamic指令到底做了什么事情呢?

    java/util/List.forEach方法接收一个参数Consumer<? super T> action,Consumer是一个接口,所以如果要调用这个方法,就要传递该接口类型的对象。

    而我们在代码里实际上是传递的一个Lambda表达式,那么我们这里可以假设:需要将Lambda表达式转换成对象,且该对象的类型需要根据该Lambda表达式所使用的地方在编译时期进行反推。

    这里在解释一下反推:一个Lambda表达式是可以被多个方法使用的,而且这个方法所接收的参数类型,也就是函数式接口,是可以不一样的,只要函数式接口符合该Lambda表达式的定义即可。

    本例中,编译器在编译时可以反推出,Lambda表达式对应一个Cosumer接口类型的对象。

    那么如果要将Lambda表达式转换成一个对象,就需要有一个类实现Consumer接口。

    所以,现在的问题就是这个类是什么时候生成的,并且生成在哪里了?

    所以,我们慢慢的应该能够想到,invokedynamic指令,它是不是就是先将Lambda表达式转换成某个类,然后生成一个实例以便提供给forEach方法调用呢?

    我们回头再看一下invokedynamic指令:

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

    Java中调用函数有四大指令:invokevirtual、invokespecial、invokestatic、invokeinterface,在JSR 292 添加了一个新的指令invokedynamic,这个指令表示执行动态语言,也就是Lambda表达式。

    该指令注释中的#0表示的是BootstrapMethods中的第0个方法:

    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

    所以invokedynamic执行时,实际上就是执行BootstrapMethods中的方法,比如本例中的:java/lang/invoke/LambdaMetafactory.metafactory

    代码如下:

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

    这个方法中用到了一个特别明显且易懂的类:InnerClassLambdaMetafactory。

    这个类是一个针对Lambda表达式生成内部类的工厂类。当调用buildCallSite方法是会生成一个内部类并且生成该类的一个实例。

    那么现在要生成一个内部类,需要一些什么条件呢:

    1. 类名:可按一些规则生成
    2. 类需要实现的接口:编译时就已知了,本例中就是Consumer接口
    3. 实现接口里面的方法:本例中就是Consumer接口的void accept(T t)方法。

    那么内部类该怎么实现void accept(T t)方法呢?

    我们再来看一下javap -v -p Test.class的结果中除开我们自己实现的方法外还多了一个方法:

    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;

    很明显,这个静态的lambda$main$0方法代表的就是我们写的Lambda表达式,只是因为我们例子中Lambda表达式没写什么逻辑,所以这段字节码指令Code部分也没有什么内容。

    那么,我们现在在实现内部类中的void accept(T t)方法时,只要调用一个这个lambda$main$0静态方法即可。

    所以到此,一个内部类就可以被正常的实现出来了,内部类有了之后,Lambda表达式就是可以被转换成这个内部类的对象,就可以进行循环了。

  • 相关阅读:
    leetcode 347. Top K Frequent Elements
    581. Shortest Unsorted Continuous Subarray
    leetcode 3. Longest Substring Without Repeating Characters
    leetcode 217. Contains Duplicate、219. Contains Duplicate II、220. Contains Duplicate、287. Find the Duplicate Number 、442. Find All Duplicates in an Array 、448. Find All Numbers Disappeared in an Array
    leetcode 461. Hamming Distance
    leetcode 19. Remove Nth Node From End of List
    leetcode 100. Same Tree、101. Symmetric Tree
    leetcode 171. Excel Sheet Column Number
    leetcode 242. Valid Anagram
    leetcode 326. Power of Three
  • 原文地址:https://www.cnblogs.com/lanqingzhou/p/13590427.html
Copyright © 2011-2022 走看看