Skip to content

Commit 25242c9

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

15 files changed

+477
-1
lines changed

8-Stream Performance.md

+108
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

Figures/perf_Stream_min_int_par.png

83.6 KB
Loading

Figures/perf_Stream_reduction.png

89.3 KB
Loading

Figures/perf_Stream_reduction_par.png

80 KB
Loading

README.md

+2-1
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

+6
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

+17
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>
+89
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package lee;
2+
3+
import java.util.Arrays;
4+
import java.util.Random;
5+
/**
6+
* java -server -Xms10G -Xmx10G -XX:+PrintGCDetails
7+
* -XX:+UseConcMarkSweepGC -XX:CompileThreshold=1000 lee/IntTest
8+
* taskset -c 0-[0,1,3,7] java ...
9+
* @author CarpenterLee
10+
*/
11+
public class IntTest {
12+
13+
public static void main(String[] args) {
14+
new IntTest().doTest();
15+
}
16+
public void doTest(){
17+
warmUp();
18+
int[] lengths = {
19+
10000,
20+
100000,
21+
1000000,
22+
10000000,
23+
100000000,
24+
1000000000
25+
};
26+
for(int length : lengths){
27+
System.out.println(String.format("---array length: %d---", length));
28+
int[] arr = new int[length];
29+
randomInt(arr);
30+
31+
int times = 4;
32+
int min1 = 1;
33+
int min2 = 2;
34+
int min3 = 3;
35+
long startTime;
36+
37+
startTime = System.nanoTime();
38+
for(int i=0; i<times; i++){
39+
min1 = minIntFor(arr);
40+
}
41+
TimeUtil.outTimeUs(startTime, "minIntFor time:", times);
42+
43+
startTime = System.nanoTime();
44+
for(int i=0; i<times; i++){
45+
min2 = minIntStream(arr);
46+
}
47+
TimeUtil.outTimeUs(startTime, "minIntStream time:", times);
48+
49+
startTime = System.nanoTime();
50+
for(int i=0; i<times; i++){
51+
min3 = minIntParallelStream(arr);
52+
}
53+
TimeUtil.outTimeUs(startTime, "minIntParallelStream time:", times);
54+
55+
56+
System.out.println(min1==min2 && min2==min3);
57+
}
58+
}
59+
private void warmUp(){
60+
int[] arr = new int[100];
61+
randomInt(arr);
62+
for(int i=0; i<20000; i++){
63+
// minIntFor(arr);
64+
minIntStream(arr);
65+
minIntParallelStream(arr);
66+
67+
}
68+
}
69+
private int minIntFor(int[] arr){
70+
int min = Integer.MAX_VALUE;
71+
for(int i=0; i<arr.length; i++){
72+
if(arr[i]<min)
73+
min = arr[i];
74+
}
75+
return min;
76+
}
77+
private int minIntStream(int[] arr){
78+
return Arrays.stream(arr).min().getAsInt();
79+
}
80+
private int minIntParallelStream(int[] arr){
81+
return Arrays.stream(arr).parallel().min().getAsInt();
82+
}
83+
private void randomInt(int[] arr){
84+
Random r = new Random();
85+
for(int i=0; i<arr.length; i++){
86+
arr[i] = r.nextInt();
87+
}
88+
}
89+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
package lee;
2+
3+
import java.util.ArrayList;
4+
import java.util.HashMap;
5+
import java.util.List;
6+
import java.util.Map;
7+
import java.util.Random;
8+
import java.util.UUID;
9+
import java.util.stream.Collectors;
10+
11+
/**
12+
* java -server -Xms10G -Xmx10G -XX:+PrintGCDetails
13+
* -XX:+UseConcMarkSweepGC -XX:CompileThreshold=1000 lee/ReductionTest
14+
* taskset -c 0-[0,1,3,7] java ...
15+
* @author CarpenterLee
16+
*/
17+
public class ReductionTest {
18+
19+
public static void main(String[] args) {
20+
new ReductionTest().doTest();
21+
}
22+
public void doTest(){
23+
warmUp();
24+
int[] lengths = {
25+
10000,
26+
100000,
27+
1000000,
28+
10000000,
29+
20000000,
30+
40000000
31+
};
32+
for(int length : lengths){
33+
System.out.println(String.format("---orders length: %d---", length));
34+
List<Order> orders = Order.genOrders(length);
35+
int times = 4;
36+
Map<String, Double> map1 = null;
37+
Map<String, Double> map2 = null;
38+
Map<String, Double> map3 = null;
39+
40+
long startTime;
41+
42+
startTime = System.nanoTime();
43+
for(int i=0; i<times; i++){
44+
map1 = sumOrderForLoop(orders);
45+
}
46+
TimeUtil.outTimeUs(startTime, "sumOrderForLoop time:", times);
47+
48+
startTime = System.nanoTime();
49+
for(int i=0; i<times; i++){
50+
map2 = sumOrderStream(orders);
51+
}
52+
TimeUtil.outTimeUs(startTime, "sumOrderStream time:", times);
53+
54+
startTime = System.nanoTime();
55+
for(int i=0; i<times; i++){
56+
map3 = sumOrderParallelStream(orders);
57+
}
58+
TimeUtil.outTimeUs(startTime, "sumOrderParallelStream time:", times);
59+
60+
System.out.println("users=" + map3.size());
61+
62+
}
63+
}
64+
private void warmUp(){
65+
List<Order> orders = Order.genOrders(10);
66+
for(int i=0; i<20000; i++){
67+
sumOrderForLoop(orders);
68+
sumOrderStream(orders);
69+
sumOrderParallelStream(orders);
70+
71+
}
72+
}
73+
private Map<String, Double> sumOrderForLoop(List<Order> orders){
74+
Map<String, Double> map = new HashMap<>();
75+
for(Order od : orders){
76+
String userName = od.getUserName();
77+
Double v;
78+
if((v=map.get(userName)) != null){
79+
map.put(userName, v+od.getPrice());
80+
}else{
81+
map.put(userName, od.getPrice());
82+
}
83+
}
84+
return map;
85+
}
86+
private Map<String, Double> sumOrderStream(List<Order> orders){
87+
return orders.stream().collect(
88+
Collectors.groupingBy(Order::getUserName,
89+
Collectors.summingDouble(Order::getPrice)));
90+
}
91+
private Map<String, Double> sumOrderParallelStream(List<Order> orders){
92+
return orders.parallelStream().collect(
93+
Collectors.groupingBy(Order::getUserName,
94+
Collectors.summingDouble(Order::getPrice)));
95+
}
96+
}
97+
class Order{
98+
private String userName;
99+
private double price;
100+
private long timestamp;
101+
public Order(String userName, double price, long timestamp) {
102+
this.userName = userName;
103+
this.price = price;
104+
this.timestamp = timestamp;
105+
}
106+
public String getUserName() {
107+
return userName;
108+
}
109+
public double getPrice() {
110+
return price;
111+
}
112+
public long getTimestamp() {
113+
return timestamp;
114+
}
115+
public static List<Order> genOrders(int listLength){
116+
ArrayList<Order> list = new ArrayList<>(listLength);
117+
Random rand = new Random();
118+
int users = listLength/200;// 200 orders per user
119+
users = users==0 ? listLength : users;
120+
ArrayList<String> userNames = new ArrayList<>(users);
121+
for(int i=0; i<users; i++){
122+
userNames.add(UUID.randomUUID().toString());
123+
}
124+
for(int i=0; i<listLength; i++){
125+
double price = rand.nextInt(1000);
126+
String userName = userNames.get(rand.nextInt(users));
127+
list.add(new Order(userName, price, System.nanoTime()));
128+
}
129+
return list;
130+
}
131+
@Override
132+
public String toString(){
133+
return userName + "::" + price;
134+
}
135+
}

0 commit comments

Comments
 (0)