Java 8 Stream的性能到底如何?

逻辑织光使
• 阅读 40522

Java 8提供的流的基于Lambda表达式的函数式的操作写法让人感觉很爽,笔者也一直用的很开心,直到看到了Java8 Lambda表达式和流操作如何让你的代码变慢5倍,笔者当时是震惊的,我读书少,你不要骗我。瞬间我似乎为我的Server Application速度慢找到了一个很好地锅,不过这个跟书上讲的不一样啊。于是笔者追本溯源,最后找到了始作俑者自己的分析:原文

不久之前我在社区内发表了这篇文章: I mused about the performance of Java 8 streams ,上面的测试结果貌似很有道理。其中一个测试是将传统的for-循环与Stream进行了比较。很多人表示了震惊、不相信等等很多很多的情绪,甚至有人直接说Stream是个什么鬼,哪凉快哪呆着去。这是没有道理的,毕竟不能通过一个简单地只是一个环境下的测试就否定这些。

在之前的测评中,在500,000个随机的整形数的数组的遍历中,我们得出的结论是for-循环的速度会比Stream的速度快上15倍。其中for-循环的数组如下所示:

int[] a = ints;
int e = ints.length;
int m = Integer.MIN_VALUE;
 
for (int i = 0; i < e; i++)
    if (a[i] > m) m = a[i];

同样的,我们建立了一个原始类型的IntStream

int m = Arrays.stream(ints)
              .reduce(Integer.MIN_VALUE, Math::max);

在我们这个过时的设备上(双核)跑出来的结果是:

int-array, for-loop : 0.36 ms
int-array, seq. stream: 5.35 ms

for循环的方式明显的比Stream流要快很多很多,然后我们选择了另一台4核的设备,发现这个比例因子变成了4.2(原来是15)。这个结果的详细信息可以看Nicolai Parlog’s blog 这个文章:

Java 8 Stream的性能到底如何?

正如我们所料,不同的环境可能会引发不同的结果。不过我们评测的核心:for-循环速度奏是比Stream快,在不同的平台上是一致的。

接下来,我们不再测试原始类型,改用了ArrayList<Integer>,同样是填充了500000个随机数,然后结果是:

ArrayList, for-loop   : 6.55 ms
ArrayList, seq. stream: 8.33 ms

是的,for-循环的速度确实还是会快一点,但是很明显这种差距缩小了。这个结果并不令人惊讶,实际上整个测试的性能主要取决于内存访问与遍历这两大块。其中内存访问这个还受限制于硬件本身,所以不同的平台上会有不同的结果。实际上在我们的测试中出现这样的结果并不会令人惊讶,毕竟我们特意选择了一个比较极端的情况,代表了范围内的某个极端,可以解释如下:

  • 我们将for-loops与Streams进行了比较。循环本身是JIT友好的。编译器本身有了40年以上的经验,然后我们选择了循环这个JIT编译器重点优化的部分。这是所谓的某个极端:一个JIT友好的,高度优化的访问序列元素的方法。而如果是使用流的话也就意味着会在主框架内进行调用,不可避免地增加内存调用。而一个JIT编译器本身是有一个上限的,虽然大部分情况下是用不满的。因此,我们将这种情况分为JIT友好与不友好,而for-循环本身是处于JIT友好的这一边,因此它自然能够赢得这个测试,并没有神马奇怪。

  • 我们将原始类型的序列与引用类型的序列进行了比较。这两种情况可以用缓存友好/不友好来区分。一个原始类型int的序列是非常缓存友好的,特别是当未来Java引入不可变序列的时候。而一个引用类型的序列,即使用了基于数组的,就像ArrayList的这样的存储,也是只有很小的概率进行很好地缓存。每次独立地对于序列成员的访问需要获取指针指向的地址然后获取其内容,也就意味着缓存的失效。很明显地,一个使用了int[]for-循环肯定处在缓存友好这一边,自然与序列引用的Stream相比性能上要好上很多。

  • 我们将元素轻量级使用与CPU密集型使用相比。更重要的是,我们将这种两个两个的比较寻找最大值的计算与Taylor相似度下寻找正弦值的计算进行了比较。在下面一个实验中,我们会以相对而言复杂一点的CPU密集型的运算为例,可能获取到这个值需要一分钟的时间。我们将此称作CPU友好或者CPU不友好的分割。一般来说,对于序列中的元素进行重量级的CPU密集型的运算的时候,也就是所谓的CPU不友好运算时,评测结果往往由CPU的运算速度决定,而对于上面讲的缓存缺失以及JIT循环的优化就变得不那么重要了。

讲了这么多,我们已经可以发现上面评测中对于int[]类型的数组中寻找最大值的这件事是受到JIT友好以及缓存友好这两个因素决定的。这种情况下,当然for-循环会占了很大优势,如果没做到这样才会让人惊讶呢。那么如果我们对于上文中所讲的CPU密集型的情况,这也是一种极端情况,进行评测,其中for-循环式这样的:

int[] a = ints;
int e = a.length;
double m = Double.MIN_VALUE;
 
for (int i = 0; i < e; i++) {
   double d = Sine.slowSin(a[i]);
   if (d > m) m = d;
}

Stream的用法如下:

Arrays.stream(ints)
      .mapToDouble(Sine::slowSin)
      .reduce(Double.MIN_VALUE, (i, j) -> Math.max(i, j));

最后的结果是:

for``-loop   : ``11.82` `ms
seq. stream: ``12.15` `ms

这个评测依旧是在上文说到的那个老旧的机器上进行的。确实for-循环的效率是比Stream要快的,不过可以看得出来这种差距不再明显了。换种说法,这种差距在统计评测的角度来看还是很重要的,不过在实际的应用过程中已经无足轻重了。到这里我们证明了与上次评测相悖的一个观点:其实Stream与for-循环之间的性能并木有很大的差异。

最后来总结一波,在有些情况下,Stream的效率确实会比for-循环要慢上很多倍,然后在其他大部分情况下是没有虾米差异的。你可以觉得Stream很酷然后就去使用它,或者为了优化你的应用的性能而依旧选择旧的语法。同时,也不要无缘无故就觉得人家Stream损害了你应用的性能,那是你自己用得不好。

点赞
收藏
评论区
推荐文章
blmius blmius
4年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
美凌格栋栋酱 美凌格栋栋酱
7个月前
Oracle 分组与拼接字符串同时使用
SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Wesley13 Wesley13
3年前
Java日期时间API系列31
  时间戳是指格林威治时间1970年01月01日00时00分00秒起至现在的总毫秒数,是所有时间的基础,其他时间可以通过时间戳转换得到。Java中本来已经有相关获取时间戳的方法,Java8后增加新的类Instant等专用于处理时间戳问题。 1获取时间戳的方法和性能对比1.1获取时间戳方法Java8以前
Wesley13 Wesley13
3年前
java 8 lambda表达式中的异常处理
java8lambda表达式中的异常处理简介java8中引入了lambda表达式,lambda表达式可以让我们的代码更加简介,业务逻辑更加清晰,但是在lambda表达式中使用的FunctionalInterface并没有很好的处理异常,因为JDK提供的这些FunctionalInterface通常都是没有抛出异常的,这意味着需要我们自
Wesley13 Wesley13
3年前
FLV文件格式
1.        FLV文件对齐方式FLV文件以大端对齐方式存放多字节整型。如存放数字无符号16位的数字300(0x012C),那么在FLV文件中存放的顺序是:|0x01|0x2C|。如果是无符号32位数字300(0x0000012C),那么在FLV文件中的存放顺序是:|0x00|0x00|0x00|0x01|0x2C。2.  
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
3年前
PHP创建多级树型结构
<!lang:php<?php$areaarray(array('id'1,'pid'0,'name''中国'),array('id'5,'pid'0,'name''美国'),array('id'2,'pid'1,'name''吉林'),array('id'4,'pid'2,'n
Wesley13 Wesley13
3年前
Java 8 stream 实战
概述平时工作用python的机会比较多,习惯了python函数式编程的简洁和优雅。切换到java后,对于数据处理的『冗长代码』还是有点不习惯的。有幸的是,Java8版本后,引入了Lambda表达式和流的新特性,当流和Lambda表达式结合起来一起使用时,因为流申明式处理数据集合的特点,可以让代码变得简洁易读。幸福感爆棚,有没有!本文主要列举一些
Python进阶者 Python进阶者
1年前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这
逻辑织光使
逻辑织光使
Lv1
阶下青苔与红树,雨中寥落月中愁。
文章
3
粉丝
0
获赞
0