资讯专栏INFORMATION COLUMN

Java 8 Stream的性能到底如何?

Sike / 585人阅读

摘要:而一个编译器本身是有一个上限的,虽然大部分情况下是用不满的。我们将此称作友好或者不友好的分割。同时,也不要无缘无故就觉得人家损害了你应用的性能,那是你自己用得不好。

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 这个文章:

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

接下来,我们不再测试原始类型,改用了ArrayList,同样是填充了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损害了你应用的性能,那是你自己用得不好。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/65311.html

相关文章

  • Java8实战》-第四章读书笔记(引入流Stream

    摘要:内部迭代与使用迭代器显式迭代的集合不同,流的迭代操作是在背后进行的。流只能遍历一次请注意,和迭代器类似,流只能遍历一次。 流(Stream) 流是什么 流是Java API的新成员,它允许你以声明性方式处理数据集合(通过查询语句来表达,而不是临时编写一个实现)。就现在来说,你可以把它们看成遍历数据集的高级迭代器。此外,流还可以透明地并行处理,你无需写任何多线程代码了!我会在后面的笔记中...

    _ivan 评论0 收藏0
  • Java 8 vs. Scala(一): Lambda表达式

    摘要:编程语言将函数作为一等公民,函数可以被作为参数或者返回值传递,因为它被视为对象。是表示已注释接口是函数接口的注释。如果一个函数有一个或多个参数并且有返回值呢为了解决这个问题,提供了一系列通用函数接口,在包里。 【编者按】虽然 Java 深得大量开发者喜爱,但是对比其他现代编程语言,其语法确实略显冗长。但是通过 Java8,直接利用 lambda 表达式就能编写出既可读又简洁的代码。作者...

    yuanxin 评论0 收藏0
  • 推荐:7 月份值得一看 Java 技术干货!

    摘要:月底了,又到了我们总结这一个月技术干货的时候了,又到了我们给粉丝免费送书的日子了。 月底了,又到了我们总结这一个月 Java 技术干货的时候了,又到了我们给粉丝免费送书的日子了。 7 月份干货总结 Oracle 发布了一个全栈虚拟机 GraalVM 一文带你深入拆解 Java 虚拟机 图文带你了解 8 大排序算法 Spring Boot 2.x 新特性总结及迁移指南 Spring B...

    saucxs 评论0 收藏0
  • Java8实战》-读书笔记第一章(02)

    摘要:实战读书笔记第一章从方法传递到接着上次的,继续来了解一下,如果继续简化代码。去掉并且生成的数字是万,所消耗的时间循序流并行流至于为什么有时候并行流效率比循序流还低,这个以后的文章会解释。 《Java8实战》-读书笔记第一章(02) 从方法传递到Lambda 接着上次的Predicate,继续来了解一下,如果继续简化代码。 把方法作为值来传递虽然很有用,但是要是有很多类似与isHeavy...

    lushan 评论0 收藏0
  • Java 8 Strem基本操作

    摘要:可以使用方法替换常规循环以上代码的产出所有这些原始流都像常规对象流一样工作,但有以下不同之处原始流使用专门的表达式,例如代替或代替。原始流支持额外的终端聚合操作,以上代码的产出有时将常规对象流转换为基本流是有用的,反之亦然。 本文提供了有关Java 8 Stream的深入概述。当我第一次读到的Stream API,我感到很困惑,因为它听起来类似Java I/O的InputStream,...

    Jensen 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<