前言:
我们先要问自己一个问题: 并行化运行基于流的代码是否比串行化运行更快? 这不是一个简单的问题。 回到前面的例子, 哪种方式花的时间更多取决于串行或并行化运行时的环境。
以例 6-1 和例 6-2 中的代码为准, 在一个四核电脑上, 如果有 10 张专辑, 串行化代码的速度是并行化代码速度的 8 倍; 如果将专辑数量增至 100 张, 串行化和并行化速度相当; 如果将专辑数量增值 10 000 张, 则并行化代码的速度是串行化代码速度的 2.5 倍。
在前面我简要提及了影响并行流是否比串行流快的一些因素, 现在让我们仔细看看它们。理解哪些能工作、 哪些不能工作, 能帮助在如何使用、 什么时候使用并行流这一问题上做出明智的决策。 影响并行流性能的主要因素有 5 个, 依次分析如下。
(1)数据大小
输入数据的大小会影响并行化处理对性能的提升。 将问题分解之后并行化处理, 再将结果合并会带来额外的开销。 因此只有数据足够大、 每个数据处理管道花费的时间足够多时, 并行化处理才有意义。 6.3 节讨论过。
(2)源数据结构
每个管道的操作都基于一些初始数据源, 通常是集合。 将不同的数据源分割相对容易,这里的开销影响了在管道中并行处理数据时到底能带来多少性能上的提升。
(3)装箱
处理基本类型比处理装箱类型要快。
(4)核的数量
极端情况下, 只有一个核, 因此完全没必要并行化。 显然, 拥有的核越多, 获得潜在性能提升的幅度就越大。 在实践中, 核的数量不单指你的机器上有多少核, 更是指运行时你的机器能使用多少核。 这也就是说同时运行的其他进程, 或者线程关联性强制线程在某些核或 CPU 上运行) 会影响性能。
(5)单元处理开销
比如数据大小, 这是一场并行执行花费时间和分解合并操作开销之间的战争。 花在流中每个元素身上的时间越长, 并行操作带来的性能提升越明显。
我们可以根据性能的好坏, 将核心类库提供的通用数据结构分成以下 3 组。
(1) 性能好
ArrayList、 数组或 IntStream.range, 这些数据结构支持随机读取, 也就是说它们能轻
而易举地被任意分解。
(2)性能一般
HashSet、 TreeSet, 这些数据结构不易公平地被分解, 但是大多数时候分解是可能的。
(3)性能差
有些数据结构难于分解, 比如, 可能要花 O(N) 的时间复杂度来分解问题。 其中包括LinkedList, 对半分解太难了。 还有 Streams.iterate 和 BufferedReader.lines, 它们长度未知, 因此很难预测该在哪里分解。
初始的数据结构影响巨大。 举一个极端的例子, 对比对 10 000 个整数并行求和, 使用 ArrayList要比使用 LinkedList 快 10 倍。 这不是说业务逻辑的性能情况也会如此, 只是说明了数据结构对于性能的影响之大。 使用形如 LinkedList 这样难于分解的数据结构并行运行可能更慢。
如果能避开有状态, 选用无状态操作, 就能获得更好的并行性能。 无状态操作包括 map、filter 和 flatMap, 有状态操作包括 sorted、 distinct 和 limit
(1)并行化数组操作
表6-1: 数组上的并行化操作
方法名 操 作
parallelPrefix 任意给定一个函数, 计算数组的和
parallelSetAll 使用 Lambda 表达式更新数组元素
parallelSort 并行化对数组元素排序
例如:
int[] arrs=new int[10];
Arrays.parallelSetAll(arrs, i->i+1);
System.out.println(Arrays.toString(arrs));
要点回顾
(1) 数据并行化是把工作拆分, 同时在多核 CPU 上执行的方式。
(2)如果使用流编写代码, 可通过调用 parallel 或者 parallelStream 方法实现数据并行化操作。
(3) 影响性能的五要素是: 数据大小、 源数据结构、 值是否装箱、 可用的 CPU 核数量, 以及处理每个元素所花的时间。
/*************************练习题**********************/
(1)例 6-10 中的代码顺序求流中元素的平方和, 将其改为并行处理。
例 6-10 顺序求列表中数字的平方和
public static int sequentialSumOfSquares(IntStream range) {
return range.map(x -> x * x)
.sum();
}
答案:
package com.mmmm.test;
import java.util.Arrays;
import java.util.stream.IntStream;
public class Test_0506_01 {
public static void main(String[] args) {
/*************************练习题**********************/
/*1. 例 6-10 中的代码顺序求流中元素的平方和, 将其改为并行处理。
例 6-10 顺序求列表中数字的平方和*/
int[] arr1=new int[100];
Arrays.parallelSetAll(arr1, i->i);
IntStream range=IntStream.of(arr1);
long start= System.currentTimeMillis();
System.out.println(sequentialSumOfSquares(range));
long end= System.currentTimeMillis();
System.out.println("串行花费的时间为:"+(end-start)+"秒");
IntStream range2=IntStream.of(arr1);
long start2= System.currentTimeMillis();
System.out.println(parallelSumOfSquares(range2));
long end2= System.currentTimeMillis();
System.out.println("并行花费的时间为:"+(end2-start2)+"秒");
}
public static long sequentialSumOfSquares(IntStream range) {
return range.map(x -> x * x).sum();
}
public static long parallelSumOfSquares(IntStream range) {
return range.parallel().map(x -> x * x).sum();
}
}
练习题2
2. 例 6-11 中的代码把列表中的数字相乘, 然后再将所得结果乘以 5。 顺序执行这段程序没有问题, 但并行执行时有一个缺陷, 使用流并行化执行该段代码, 并修复缺陷。
例 6-11 把列表中的数字相乘, 然后再将所得结果乘以 5, 该实现有一个缺陷
public static int multiplyThrough(List<Integer> linkedListOfNumbers) {
return linkedListOfNumbers.stream()
.reduce(5, (acc, x) -> x * acc);
}
答案:
package com.mmmm.test;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class Test_0506_02 {
public static void main(String[] args) {
// TODO Auto-generated method stub
List<Integer> linkedListOfNumbers = new ArrayList<>();
Integer[] arr = new Integer[10];
Arrays.parallelSetAll(arr, i -> i + 1);
linkedListOfNumbers = Arrays.asList(arr);
System.out.println(multiplyThrough(linkedListOfNumbers));
System.out.println(parallelMultiplyThrough(linkedListOfNumbers));
}
/**
* 并行
*
* @param linkedListOfNumbers
* @return
*/
public static int multiplyThrough(List<Integer> linkedListOfNumbers) {
return linkedListOfNumbers.stream().reduce(5, (acc, x) -> x * acc);
}
/**
* 串行 因为如果串行的话,比如我有四个核,那么我就把数据分成四部分,那么每次的初始值都是5,也就是我把5乘了4遍,可是题意应该是乘以一遍即可;
* 所以可以把5放在最后计算完成之后再乘
*
* @param linkedListOfNumbers
* @return
*/
public static int parallelMultiplyThrough(List<Integer> linkedListOfNumbers) {
return 5 * linkedListOfNumbers.parallelStream().reduce(1, (acc, x) -> x * acc);
}}
练习题3
3. 例 6-12 中的代码计算列表中数字的平方和。 尝试改进代码性能, 但不得牺牲代码质量。只需要一些简单的改动即可。
例 6-12 求列表元素的平方和, 该实现方式性能不高
public int slowSumOfSquares() {
return linkedListOfNumbers.parallelStream()
.map(x -> x * x)
.reduce(0, (acc, x) -> acc + x);
}
答案:
package com.mmmm.test;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class Test_0506_03 {
public static void main(String[] args) {
// TODO Auto-generated method stub
/*
* 3. 例 6-12 中的代码计算列表中数字的平方和。 尝试改进代码性能, 但不得牺牲代码质量。 只需要一些简单的改动即可。 例 6-12
* 求列表元素的平方和, 该实现方式性能不高
*/
List<Integer> linkedListOfNumbers = new ArrayList<>();
Integer[] arr = new Integer[10];
Arrays.parallelSetAll(arr, i -> i + 1);
linkedListOfNumbers = Arrays.asList(arr);
long start = System.currentTimeMillis();
System.out.println(slowSumOfSquares(linkedListOfNumbers));
long end = System.currentTimeMillis();
System.out.println("慢的方法花费的时间是:"+(end-start)+"秒");
long start2 = System.currentTimeMillis();
System.out.println(quickSumOfSquares(linkedListOfNumbers));
long end2 = System.currentTimeMillis();
System.out.println("快的方法花费的时间是:"+(end2-start2)+"秒");
}
public static int slowSumOfSquares(List<Integer> linkedListOfNumbers) {
return linkedListOfNumbers.parallelStream().map(x -> x * x).reduce(0, (acc, x) -> acc + x);
}
public static int quickSumOfSquares(List<Integer> linkedListOfNumbers) {
return linkedListOfNumbers.parallelStream().mapToInt(x -> x * x).sum();
}
}
运行结果:
385
慢的方法花费的时间是:5秒
385
快的方法花费的时间是:2秒
所以,最后求和的话可以直接使用mapToInt以后直接sum()方法即可。