请提供参考,为什么下面两个使用Java Stream API的析因实现之间的执行时间会有显着差异:
我的期望是接近执行时间,但是并行版本的执行速度提高了2倍。 我没有运行任何专门的基准测试,但是即使在冷启动的jvm中,执行时间也应该相差无几。 下面,我附上两个实现的源代码:
public class FastFactorialSupplier implements FactorialSupplier {
private final ExecutorService executorService;
public FastFactorialSupplier(ExecutorService executorService) {
this.executorService = executorService;
}
@Override
public BigInteger get(long k) {
try {
return executorService
.submit(
() -> LongStream.range(2, k + 1)
.parallel()
.mapToObj(BigInteger::valueOf)
.reduce(BigInteger.ONE, (current, factSoFar) -> factSoFar.multiply(current))
)
.get();
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
return BigInteger.ZERO;
}
}
public class MathUtils {
public static BigInteger factorial(long k) {
return LongStream.range(2, k + 1)
.mapToObj(BigInteger::valueOf)
.reduce(BigInteger.ONE, (current, factSoFar) -> factSoFar.multiply(current));
}
}
以下是根据intellij junit运行器显示的内容附有示例执行时间的测试用例作为注释。
@Test
public void testWithoutParallel() {
//2s 403
runTest(new DummyFactorialSupplier()); // uses MathUtils.factorial
}
@Test
public void testParallelismWorkStealing1() {
//1s 43
runTest(new FastFactorialSupplier(Executors.newWorkStealingPool(1)));
}
@Test
public void testParallelismForkJoin1() {
// 711ms
runTest(new FastFactorialSupplier(new ForkJoinPool(1)));
}
@Test
public void testExecutorForkJoin() {
//85ms
runTest(new FastFactorialSupplier(new ForkJoinPool()));
}
private void runTest(FactorialSupplier factorialSupplier) {
BigInteger result = factorialSupplier.get(100000);
assertNotNull(result);
// assertEquals(456574, result.toString().length());
}
由于使用自定义派生连接池的Java 8中存在问题-https://bugs.openjdk.java.net/browse/JDK-8190974
,因此使用Java 11运行测试是否可以进行与伪并行处理有关的优化以及执行的调度方式,而考虑到执行纯粹是顺序执行,则没有这样的优化方式吗?
编辑:
我还使用jmh运行微基准测试
平行:
public class FastFactorialSupplierP1Test {
@Benchmark
@BenchmarkMode({Mode.AverageTime, Mode.SampleTime, Mode.SingleShotTime, Mode.Throughput, Mode.All})
@Fork(value = 1, warmups = 1)
public void measure() {
runTest(new FastFactorialSupplier(new ForkJoinPool(1)));
}
private void runTest(FactorialSupplier factorialSupplier) {
BigInteger result = factorialSupplier.get(100000);
assertNotNull(result);
}
public static void main(String[] args) throws Exception {
org.openjdk.jmh.Main.main(args);
}
}
序列号:
public class SerialFactorialSupplierTest {
@Benchmark
@BenchmarkMode({Mode.AverageTime, Mode.SampleTime, Mode.SingleShotTime, Mode.Throughput, Mode.All})
@Fork(value = 1, warmups = 1)
public void measure() {
runTest(new DummyFactorialSupplier());
}
private void runTest(FactorialSupplier factorialSupplier) {
BigInteger result = factorialSupplier.get(100000);
assertNotNull(result);
}
public static void main(String[] args) throws Exception {
org.openjdk.jmh.Main.main(args);
}
}
public class IterativeFactorialTest {
@Benchmark
@BenchmarkMode({Mode.AverageTime, Mode.SampleTime, Mode.SingleShotTime, Mode.Throughput, Mode.All})
@Fork(value = 1, warmups = 1)
public void measure() {
runTest(new IterativeFact());
}
private void runTest(FactorialSupplier factorialSupplier) {
BigInteger result = factorialSupplier.get(100000);
assertNotNull(result);
}
public static void main(String[] args) throws Exception {
org.openjdk.jmh.Main.main(args);
}
class IterativeFact implements FactorialSupplier {
@Override
public BigInteger get(long k) {
BigInteger result = BigInteger.ONE;
while (k-- != 0) {
result = result.multiply(BigInteger.valueOf(k));
}
return result;
}
}
}
结果:
FastFactorialSupplierP1Test.measure avgt 5 0.437 ± 0.006 s/op
IterativeFactorialTest.measure avgt 5 2.643 ± 0.383 s/op
SerialFactorialSupplierTest.measure avgt 5 2.226 ± 0.044 s/op
答案 0 :(得分:5)
您选择了一种操作,其性能取决于评估的顺序。只需考虑BigInteger.multiply
的性能取决于两个因素的大小。然后,遍历一系列BigInteger
实例,并将累加值作为下一个乘法的一个因素,这将使运算越来越昂贵,而且距离您越远。
相反,当您将值的范围分成较小的范围,分别对每个范围执行乘法,并将范围的结果相乘时,即使这些子范围未同时求值,也可以获得性能优势。 / p>
因此,当并行流将工作拆分为多个块,以可能被其他工作线程拾取,但最终在同一线程中对其进行评估时,在这种特定设置下,您仍然可以获得性能的改进
em>,因为评估顺序已更改。我们可以通过删除所有与Stream和线程池相关的工件来进行测试:
public static BigInteger multiplyAll(long from, long to, int split) {
if(split < 1 || to - from < 2) return serial(from, to);
split--;
long middle = (from + to) >>> 1;
return multiplyAll(from, middle, split).multiply(multiplyAll(middle, to, split));
}
private static BigInteger serial(long l1, long l2) {
BigInteger bi = BigInteger.valueOf(l1++);
for(; l1 < l2; l1++) {
bi = bi.multiply(BigInteger.valueOf(l1));
}
return bi;
}
我手头没有设置JMH来发布压力较大的结果,但是一次简单的运行显示,数量级与您的结果相符,仅进行一次拆分就已经将评估时间缩短了一半,而更高的数值仍然可以改善性能尽管曲线变得更平坦。
如ForkJoinTask.html#getSurplusQueuedTaskCount()
中所述,这是一项合理的策略,用于拆分工作,以使每个工作人员有一些额外的任务,可能会被其他线程承担,这可能会补偿不平衡的工作负载,例如如果某些元素比其他元素便宜。显然,并行流没有用于处理没有其他工作线程的情况的特殊代码,因此,即使只有一个线程来处理它,您也可以看到拆分工作的效果。