Skip to content

Commit 25242c9

Browse files
committed
加入Steam API性能测试
1 parent 6941ec3 commit 25242c9

15 files changed

Lines changed: 477 additions & 1 deletion

8-Stream Performance.md

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
# Stream Performance
2+
3+
已经对Stream API的用法鼓吹够多了,用起简洁直观,但性能到底怎么样呢?会不会有很高的性能损失?本节我们对Stream API的性能一探究竟。
4+
5+
为保证测试结果真实可信,我们将JVM运行在`-server`模式下,测试数据在GB量级,测试机器采用常见的商用服务器,配置如下:
6+
7+
<table width="300px"><tr><td>OS</td><td>CentOS 6.7 x86_64</td></tr><tr><td>CPU</td><td>Intel Xeon X5675, 12M Cache 3.06 GHz, 6 Cores 12 Threads</td></tr><tr><td>内存</td><td>96GB</td></tr><tr><td>JDK</td><td>java version 1.8.0_91, Java HotSpot(TM) 64-Bit Server VM</td></tr></table>
8+
9+
测试[所用代码在这里](./perf/StreamBenchmark/src/lee),测试[结果汇总](./perf/Stream_performance.xlsx).
10+
11+
## 测试方法和测试数据
12+
13+
性能测试并不是容易的事,Java性能测试更费劲,因为虚拟机对性能的影响很大,JVM对性能的影响有两方面:
14+
15+
1. GC的影响。GC的行为是Java中很不好控制的一块,为增加确定性,我们手动指定使用CMS收集器,并使用10GB固定大小的堆内存。集体到JVM参数就是`-XX:+UseConcMarkSweepGC -Xms10G -Xmx10G`
16+
2. JIT(Just-In-Time)即时编译技术。即时编译技术会将热点代码在JVM运行的过程中编译成本地代码,测试时我们会先对程序预热,触发对测试函数的即时编译。相关的JVM参数是`-XX:CompileThreshold=10000`
17+
18+
测试数据由程序随机生成。为防止一次测试带来的抖动,测试4次求出平均时间作为运行时间。
19+
20+
21+
22+
## 实验一 基本类型迭代
23+
24+
测试内容:找出整型数组中的最小值。对比for循环外部迭代和Stream API内部迭代性能。
25+
26+
测试程序[IntTest](./perf/StreamBenchmark/src/lee/IntTest.java),测试结果如下图:
27+
28+
<img src="./Figures/perf_Stream_min_int.png" width="500px" align="center" alt="perf_Stream_min_int"/>
29+
30+
图中展示的是for循环外部迭代耗时为基准的时间比值。分析如下:
31+
32+
1. 对于基本类型Stream串行迭代的性能开销明显高于外部迭代开销(两倍);
33+
2. Stream并行迭代的性能比串行迭代和外部迭代都好。
34+
35+
并行迭代性能跟跟可利用的核数有关,所有我们专门测试了不同核数下的Stream并行迭代效果:
36+
37+
<img src="./Figures/perf_Stream_min_int_par.png" width="500px" align="center" alt="perf_Stream_min_int_par"/>
38+
39+
分析,对于基本类型:
40+
41+
1. 使用Stream并行API在单核情况下性能很差,比Stream串行API的性能还差;
42+
2. 随着使用核数的增加,Stream并行效果逐渐变好,比使用for循环外部迭代的性能还好。
43+
44+
以上两个测试说明,对于基本类型的简单迭代,Stream串行迭代性能更差,但多核情况下Stream迭代时性能较好。
45+
46+
47+
## 实验二 对象迭代
48+
49+
再来看对象的迭代效果。
50+
51+
测试内容:找出字符串列表中最小的元素(自然顺序),对比for循环外部迭代和Stream API内部迭代性能。
52+
53+
测试程序[StringTest](./perf/StreamBenchmark/src/lee/StringTest.java),测试结果如下图:
54+
55+
<img src="./Figures/perf_Stream_min_String.png" width="500px" align="center" alt="perf_Stream_min_String"/>
56+
57+
结果分析如下:
58+
59+
1. 对于对象类型Stream串行迭代的性能开销仍然高于外部迭代开销(1.5倍),但差距没有基本类型那么大。
60+
2. Stream并行迭代的性能比串行迭代和外部迭代都好。
61+
62+
再来单独考察Stream并行迭代效果:
63+
64+
<img src="./Figures/perf_Stream_min_String_par.png" width="500px" align="center" alt="perf_Stream_min_String_par"/>
65+
66+
分析,对于对象类型:
67+
68+
1. 使用Stream并行API在单核情况下性能比for循环外部迭代差;
69+
2. 随着使用核数的增加,Stream并行效果逐渐变好,多核带来的效果明显。
70+
71+
以上两个测试说明,对于对象类型的简单迭代,Stream串行迭代性能更差,但多核情况下Stream迭代时性能较好。
72+
73+
## 实验三 复杂对象归约
74+
75+
从实验一、二的结果来看,Stream串行执行的效果都比外部迭代差(很多),是不是说明Stream真的不行了?先别下结论,我们再来考察一下更复杂的操作。
76+
77+
测试内容:给定订单列表,统计每个用户的总交易额。对比使用外部迭代手动实现和Stream API之间的性能。
78+
79+
我们将订单简化为`<userName, price, timeStamp>`构成的元组,并用`Order`对象来表示。测试程序[ReductionTest](./perf/StreamBenchmark/src/lee/ReductionTest.java),测试结果如下图:
80+
81+
<img src="./Figures/perf_Stream_reduction.png" width="500px" align="center" alt="perf_Stream_reduction"/>
82+
83+
分析,对于复杂的归约操作:
84+
85+
1. Stream API的性能普遍好于外部手动迭代,并行Stream效果更佳;
86+
87+
再来考察并行度对并行效果的影响,测试结果如下:
88+
89+
<img src="./Figures/perf_Stream_reduction_par.png" width="500px" align="center" alt="perf_Stream_reduction_par"/>
90+
91+
分析,对于复杂的归约操作:
92+
93+
1. 使用Stream并行归约在单核情况下性能比串行归约以及手动归约都要差,简单说就是最差的;
94+
2. 随着使用核数的增加,Stream并行效果逐渐变好,多核带来的效果明显。
95+
96+
以上两个实验说明,对于复杂的归约操作,Stream串行归约效果好于手动归约,在多核情况下,并行归约效果更佳。我们有理由相信,对于其他复杂的操作,Stream API也能表现出相似的性能表现。
97+
98+
99+
## 结论
100+
101+
上述三个实验的结果可以总结如下:
102+
103+
1. 对于简单操作,比如最简单的遍历,Stream串行API性能明显差于显示迭代,但并行的Stream API能够发挥多核特性。
104+
2. 对于复杂操作,Stream串行API性能可以和手动实现的效果匹敌,在并行执行时Stream API效果远超手动实现。
105+
106+
所以,如果出于性能(而不是代码的简洁)考虑,1. 对于简单操作推荐通过外部迭代手动实现,2. 对于复杂操作,推荐使用Stream API, 3. 在多核情况下,推荐使用并行Stream API来发挥多核优势。
107+
108+
即使是从性能方面说,尽可能的使用Stream API也另外一个优势,那就是只要Java Stream类库做了升级优化,代码不用做任何修改就能享受到升级带来的好处。

Figures/perf_Stream_min_String.png

95.6 KB
Loading
85.4 KB
Loading

Figures/perf_Stream_min_int.png

86.8 KB
Loading
83.6 KB
Loading

Figures/perf_Stream_reduction.png

89.3 KB
Loading
80 KB
Loading

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ Java 8已经发行两年多,但很多人仍然在使用JDK7。对企业来说
2626
4. [Streams API(I)](./4-Streams%20API(I).md),Stream API基本用法
2727
5. [Streams API(II)](./5-Streams%20API(II).md),Stream规约操作用法
2828
6. [Stream Pipelines](./6-Stream%20Pipelines.md),Stream流水线的实现原理
29-
7. (有待扩充)
29+
7. Stream并行实现原理
30+
7. [Stream Performance](./8-Stream%20Performance.md),Stream API性能评测
3031

3132

3233

perf/StreamBenchmark/.classpath

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<classpath>
3+
<classpathentry kind="src" path="src"/>
4+
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8"/>
5+
<classpathentry kind="output" path="bin"/>
6+
</classpath>

perf/StreamBenchmark/.project

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<projectDescription>
3+
<name>StreamBenchmark</name>
4+
<comment></comment>
5+
<projects>
6+
</projects>
7+
<buildSpec>
8+
<buildCommand>
9+
<name>org.eclipse.jdt.core.javabuilder</name>
10+
<arguments>
11+
</arguments>
12+
</buildCommand>
13+
</buildSpec>
14+
<natures>
15+
<nature>org.eclipse.jdt.core.javanature</nature>
16+
</natures>
17+
</projectDescription>

0 commit comments

Comments
 (0)