Java 8 的 Stream API 这么牛X,性能如何呢?

Stream Performance
已经对 Stream API 的用法鼓吹够多了 , 用起简洁直观 , 但性能到底怎么样呢?会不会有很高的性能损失?
本节我们对 Stream API 的性能一探究竟 。
为保证测试结果真实可信 , 我们将 JVM 运行在-server模式下 , 测试数据在 GB 量级 , 测试机器采用常见的商用服务器 , 配置如下:

Java 8 的 Stream API 这么牛X,性能如何呢?

文章插图
 
测试方法和测试数据
性能测试并不是容易的事 , JAVA 性能测试更费劲 , 因为虚拟机对性能的影响很大 , JVM 对性能的影响有两方面:
  1. GC 的影响 。GC 的行为是 Java 中很不好控制的一块 , 为增加确定性 , 我们手动指定使用 CMS 收集器 , 并使用 10GB 固定大小的堆内存 。具体到 JVM 参数就是-XX:+UseConcMarkSweepGC-Xms10G-Xmx10G
  2. JIT(Just-In-Time) 即时编译技术 。即时编译技术会将热点代码在 JVM 运行的过程中编译成本地代码 , 测试时我们会先对程序预热 , 触发对测试函数的即时编译 。相关的 JVM 参数是-XX:CompileThreshold=10000 。
Stream 并行执行时用到ForkJoinPool.commonPool()得到的线程池 , 为控制并行度我们使用 linux 的taskset命令指定 JVM 可用的核数 。
测试数据由程序随机生成 。为防止一次测试带来的抖动 , 测试 4 次求出平均时间作为运行时间 。
实验一 基本类型迭代测试内容:找出整型数组中的最小值 。对比 for 循环外部迭代和 Stream API 内部迭代性能 。
测试程序 IntTest , 测试结果如下图:
Java 8 的 Stream API 这么牛X,性能如何呢?

文章插图
 
图中展示的是 for 循环外部迭代耗时为基准的时间比值 。分析如下:
  1. 对于基本类型 Stream 串行迭代的性能开销明显高于外部迭代开销(两倍);
  2. Stream 并行迭代的性能比串行迭代和外部迭代都好 。
并行迭代性能跟可利用的核数有关 , 上图中的并行迭代使用了全部 12 个核 , 为考察使用核数对性能的影响 , 我们专门测试了不同核数下的 Stream 并行迭代效果:
Java 8 的 Stream API 这么牛X,性能如何呢?

文章插图
 
分析 , 对于基本类型:
  1. 使用 Stream 并行 API 在单核情况下性能很差 , 比 Stream 串行 API 的性能还差;
  2. 随着使用核数的增加 , Stream 并行效果逐渐变好 , 比使用 for 循环外部迭代的性能还好 。
以上两个测试说明 , 对于基本类型的简单迭代 , Stream 串行迭代性能更差 , 但多核情况下 Stream 迭代时性能较好 。
实验二 对象迭代再来看对象的迭代效果 。
测试内容:找出字符串列表中最小的元素(自然顺序) , 对比 for 循环外部迭代和 Stream API 内部迭代性能 。
测试程序 StringTest , 测试结果如下图:
Java 8 的 Stream API 这么牛X,性能如何呢?

文章插图
 
结果分析如下:
  1. 对于对象类型 Stream 串行迭代的性能开销仍然高于外部迭代开销(1.5 倍) , 但差距没有基本类型那么大 。
  2. Stream 并行迭代的性能比串行迭代和外部迭代都好 。
再来单独考察 Stream 并行迭代效果:
Java 8 的 Stream API 这么牛X,性能如何呢?

文章插图
 
分析 , 对于对象类型:
  1. 使用 Stream 并行 API 在单核情况下性能比 for 循环外部迭代差;
  2. 随着使用核数的增加 , Stream 并行效果逐渐变好 , 多核带来的效果明显 。
以上两个测试说明 , 对于对象类型的简单迭代 , Stream 串行迭代性能更差 , 但多核情况下 Stream 迭代时性能较好 。
实验三 复杂对象归约从实验一、二的结果来看 , Stream 串行执行的效果都比外部迭代差(很多) , 是不是说明 Stream 真的不行了?先别下结论 , 我们再来考察一下更复杂的操作 。
测试内容:给定订单列表 , 统计每个用户的总交易额 。对比使用外部迭代手动实现和 Stream API 之间的性能 。
我们将订单简化为<userName,price,timeStamp>构成的元组 , 并用Order对象来表示 。测试程序 ReductionTest , 测试结果如下图:


推荐阅读