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表达式就是可以被转换成这个内部类的对象,就可以进行循环了。

  • 相关阅读:
    idea中maven自动导入出现问题
    DDIA
    DDIA
    DDIA
    DDIA
    DDIA
    DDIA
    DDIA
    MIT 6.824 第五次作业Primary-Backup Replication
    MIT 6.824 第四次作业GFS
  • 原文地址:https://www.cnblogs.com/lanqingzhou/p/13590427.html
Copyright © 2011-2022 走看看