首页 > 其他分享 >lambda表达式从发烧到退烧,它为何效率低下?

lambda表达式从发烧到退烧,它为何效率低下?

时间:2024-09-19 15:52:39浏览次数:5  
标签:lang java 退烧 System currentTimeMillis lambda 表达式 Lambda

我以前做压测的时候,偶然发现lambda表达式的效率很低,但凡有lambada表达式的地方cpu指标都会超限,那么现在我来研究一下为何会如此低下(以下内容部分会参考网上其他作者的)

    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        for (int i = 0; i < 100000; i++) {
            list.add(i);
        }

        long normalStart = System.currentTimeMillis();
        for (int i = 0; i < list.size(); i++) {}
        long normalEnd = System.currentTimeMillis();
        System.out.println("for循环运行" + (normalEnd - normalStart) + "毫秒");

        long innerLambdaStart = System.currentTimeMillis();
        list.forEach(i -> {});
        long innerLambdaEnd = System.currentTimeMillis();
        System.out.println("lambda consumer在内部 运行" + (innerLambdaEnd - innerLambdaStart) + "毫秒");

    }

打印出结果:

for循环运行4毫秒

lambda consumer在内部 运行230毫秒

可以看到,lambda运行时间比普通for循环长太多了,这是为什么呢?(这里我为什么打印的是consumer在内部,这个内部的含义看到最后就明白了)

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

最后,根据上面分析的,我们把lambda的表达式的创建过程提取出来,再试一次:

    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        for (int i = 0; i < 100000; i++) {
            list.add(i);
        }

        long normalStart = System.currentTimeMillis();
        for (int i = 0; i < list.size(); i++) {}
        long normalEnd = System.currentTimeMillis();
        System.out.println("for循环运行" + (normalEnd - normalStart) + "毫秒");

        long innerLambdaStart = System.currentTimeMillis();
        list.forEach(i -> {});
        long innerLambdaEnd = System.currentTimeMillis();
        System.out.println("lambda consumer在内部 运行" + (innerLambdaEnd - innerLambdaStart) + "毫秒");

        long outLambdaStart = System.currentTimeMillis();
        Consumer consumer = i -> {};
        list.forEach(consumer);
        long outLambdaEnd = System.currentTimeMillis();
        System.out.println("lambda consumer在外部 运行" + (outLambdaEnd - outLambdaStart) + "毫秒");

    }

打印出结果:

for循环运行4毫秒

lambda consumer在内部 运行230毫秒

lambda consumer在外部 运行5毫秒

可以看到,把consumer提取到外面以后,效率大大提升,几乎可以达到和for循环一样的水平,这个也说明了lambda的耗时是在于对consumer对象的生成和解析,并且有可能每次循环都会解析一次consumer,因为我们把Consumer consumer = i -> {};这一行代码提取到打印时间的上面,结果还是一样快的.

这个结果也提醒了各位开发,如果对性能有要求,尽量不用lambada表达式,特别是简单的for循环,完全没必要用,切记不要滥用lambada表达式,如果实在要用的时候也可以把consumer先提取出来以此提高效率。

标签:lang,java,退烧,System,currentTimeMillis,lambda,表达式,Lambda
From: https://www.cnblogs.com/leecoder5/p/18420725

相关文章

  • ArcGIS标注表达式用到的字段值有空值导致标签无法显示怎么办
    数据:几个楼,包含三个字段信息,其中有的楼没有地下楼层的话,地下楼层字段值为空目标:用标注“显示名称+地上楼层+地下楼层”等信息, 遇到的问题:如果只是简单的把字段相加,地下楼层为空的要素标签不显示 然后我尝试把地下层数换成string类型,试了试还是不行,没有变化 查了下VBScr......
  • C#常用正则表达式
    来源:https://www.cnblogs.com/chaowang/p/6274852.html一、校验数字的表达式 1数字:^[0-9]*$ 2n位的数字:^\d{n}$ 3至少n位的数字:^\d{n,}$ 4m-n位的数字:^\d{m,n}$ 5零和非零开头的数字:^(0|[1-9][0-9]*)$ 6非零开头的最多带两位小数的数字:^([1-9][0-9]*)+(.[0-9]{1,2})?......
  • Lambda表达式
    1.Lambda表达式定义格式Lambda格式: ()->{} a.():重写方法的参数位置 b.->:指的是传递->将重写方法的参数传递到方法体中 c.{}:重写方法的方法体publicstaticvoidmain(String[]args){/*newThread不能省略,如果省略了,没法创建线程对......
  • Java 8 新特性:Lambda 表达式与函数式接口全面解析(OOF(面向函数编程))
    在Java8中,引入了一系列重要的新特性,极大地提升了开发者的编程体验和代码简洁性。其中,Lambda表达式和函数式接口是最具影响力的特性,尤其在推动Java进入函数式编程领域方面具有里程碑意义。本文将全面深入地讨论Lambda表达式、函数式接口(包括Java内置函数式接口与自......
  • 【JDK8新特性】Stream API 结合Lambda语法在项目中的实战应用
    Lambda语法回顾在JDK8中,Lambda表达式支持的引用类型主要有以下几种,如表1所示。种类Lambda表达式示例对应的引用示例类名引用普通方法(x,y,...)->对象名x.类普通方法名(y,...)类名::类普通方法名类名引用静态方法(x,y,...)->类名.类静态方法名(x,y,...)......
  • 超详细 正则表达式【源码解析+代码例子+图】
    由于正则表达式这个东西比较抽象,我推荐大家先看原理部分。在看原理部分如果有的表达式看不懂可以去下面看表,元字符这些东西还是比较好理解的。大家可以把我写的代码复制到编译器上跑一下,这样会更容易理解。一.基本介绍正则表达式就是用某一种模式去匹配字符串,筛选我们想要的......
  • C# 新技能 DynamicExpresso 动态表达式解析器
    目录前言项目介绍项目特点项目应用项目示例1、参数2、返回值3、生成动态委托4、Lambda表达式5、特殊标识符项目地址最后前言项目开发中有时候我们需要快速地执行一些小脚本,不想每次都去生成编译整个项目。这时如果有一个好用的动态表达式解析器那就就特别方......
  • MySQL篇(高级字符串函数/正则表达式)(持续更新迭代)
    目录讲点一:高级字符串函数一、简介二、常见字符串函数1.CONCAT()2.SUBSTRING()3.LENGTH()4.REPLACE()5.TRIM()6.UPPER()7.LOWER()8.LEFT()9.RIGHT()10.INSTR()11.LENTH(str)讲点二:正则表达式一、简介二、语法1.字符类2.重复次数3.通配符4.......
  • 生成器与lambda表达式
    目录生成器特性迭代器协议迭代器协议的工作原理使用生成器计算程序花费时间lambda函数生成器yield关键字用于创建生成器(generator)生成器是一种特殊的迭代器,不需要一次性将所有数据加载到内存中,使用yield关键字,函数可以返回一个值,然后在下一次调用时从上次返回的位置继续执......
  • 初识Lambda表达式(匿名函数)
    0.问题导向使用C++STL实现订单按照创建时间从小到大排查。usingOrder=structtagOrder{unsignedintcreateTimspec;//创建时间戳intid;//订单号inttotalPrice;//总价intstatus;//订单状态intp......