提高并行计算欧拉数的性能
Improve performance of parallel calculation of euler number
我正在尝试计算 e=∑(3−4k^2/(2k+1)!); k=0..10000
但是我卡住了,无法使用多线程获得所需的性能提升。
给定线程数,我尝试将总和分成 k / numberOfThreads
块,并为每个部分总和提交期货。
我认为不好的部分可能是阶乘计算或粒度。我尝试了一个较小的步骤,但没有得到很大的改进。也许需要一种不同的方法。
ExecutorService executor = Executors.newFixedThreadPool(numberOfThreads);
List<Future<BigDecimal>> futures = new ArrayList<>(numberOfThreads);
int step = k / numberOfThreads ;
BigDecimal result = BigDecimal.ZERO;
for (int j = 0; j <= k; j += step) {
Future<BigDecimal> future = executor.submit(new EulerCalculator(j, j + step));
futures.add(future);
}
for (Future<BigDecimal> future : futures) {
result = result.add(future.get());
}
public class EulerCalculator implements Callable<BigDecimal> {
private int start;
private int end;
public BigDecimal call() {
long numerator = 3 - 4 * start * start;
BigDecimal denominator = factorial(2 * start + 1);
BigDecimal partialSum = BigDecimal.valueOf(numerator)
.divide(denominator, 1000, RoundingMode.HALF_EVEN);
for (int i = start + 1 ; i < end; i++) {
numerator = 3 - 4 * i * i;
denominator = denominator.multiply(BigDecimal.valueOf(2 * i * (2*i + 1)));
partialSum = partialSum.add(BigDecimal.valueOf(numerator)
.divide(fact, 1000, RoundingMode.HALF_EVEN));
}
return partialSum;
}
private BigDecimal factorial(int cur) {
BigDecimal fact = BigDecimal.ONE;
for (int i = 2; i <= cur; i++) {
fact = fact.multiply(BigDecimal.valueOf(i));
}
return fact;
}
}
在四核上运行几次的最佳结果:
k = 10000
线程数 = 1:345 毫秒
线程数 = 2:216 毫秒
线程数 = 4:184 毫秒
线程数 = 8:225 毫秒
你的阶乘部分不是常数时间运算,而是 O(n)。这意味着您的第一个线程比最后一个线程的工作要少得多。因此你没有平均分配工作。
通常有三种方法可以解决这个问题。
你可以做不均匀的步长,即k越小步长越大。但是,这是非常低效的,因为您要进行数千次相同的乘法运算。
您可以尝试切换到近似算法来计算阶乘,使其达到常数时间。对于小 k,您可以使用迭代来防止精度损失,因为惩罚会很低,而且小 k 也不多。
另一种方法是构建一个大数组,其中包含所有可能用于计算的阶乘,在提交任何任务之前必须运行。这种缓存方法损失的精度较低。请参阅下面关于如何并行化此过程的评论。
由于您需要所有 denominator
,并且每个都依赖于之前的 ALL,因此我将有一个专用线程来计算所有这些;并且对于每个 denominator
计算提交一个不同的任务到你的线程池来并行计算特定的部分和。最后使用 parallel stream 聚合所有结果。以下代码显示了这些详细信息:
public static BigDecimal calculate(int k, int numberOfThreads) {
ExecutorService executor = Executors.newFixedThreadPool(numberOfThreads);
List<Future<BigDecimal>> futures = new ArrayList<>(numberOfThreads);
BigDecimal denominator = BigDecimal.ONE;
for (int j = 1; j <= k; j++) {
denominator = denominator.multiply(BigDecimal.valueOf(4 * j * j + 2 * j));
Future<BigDecimal> future = executor.submit(computePartialSum(j, denominator));
futures.add(future);
}
return futures.stream().parallel()
.map(future.get())
.reduce(BigDecimal.ZERO, BigDecimal::add).add(BigDecimal.valueOf(3));
}
public static Callable<BigDecimal> computePartialSum(int curr, BigDecimal denominator) {
return () -> {
long numerator = 3 - 4 * curr * curr;
return BigDecimal.valueOf(numerator).divide(denominator, 1000, RoundingMode.HALF_EVEN);
};
}
不过,您的瓶颈将是阶乘的计算;您可以将其划分为更小的阶乘段并缓存它们以聚合成它们的真实值,我的两分钱。
感谢您的回答!
我用一个简单的 for
循环缓存了阶乘,我得到了另一个计算的好结果:
1 thread = 17ms
2 threads = 10ms
4 threads = 7ms
但是我需要画一个类似于下图的图表,只有我利用线程计算阶乘才有可能。
我测试了这个n!
算法:
public BigDecimal calculate(int number) {
if (number == 0 || number == 1) {
return BigDecimal.ONE;
}
List<Callable<BigDecimal>> callables = new ArrayList<>();
int step = number / processors;
for (int i = 2; i <= number; i += step + 1) {
callables.add(new FactorialPartCalculator(i, i + step >= number ? number : i + step));
}
List<Future<BigDecimal>> futures = executor.invokeAll(callables);
BigDecimal result = BigDecimal.ONE;
for (Future<BigDecimal> future : futures) {
result = result.multiply(future.get());
}
return result;
}
public class FactorialPartCalculator implements Callable<BigDecimal> {
@Override
public BigDecimal call() throws Exception {
BigDecimal factorialPart = BigDecimal.ONE;
for (int i = start; i <= end; i++) {
factorialPart = factorialPart.multiply(BigDecimal.valueOf(i));
}
return factorialPart;
}
我在 20000!
的 6 个线程中获得了 6.4 倍的加速。
所以我需要缓存阶乘并将缓存过程包括在整体时间中。该程序将在 32 个处理器上进行测试,我应该获得尽可能多的加速
所以我的问题是如何更改上述算法以将所有阶乘存储在一个数组中?如果有帮助,我只需要奇数阶乘。
我正在尝试计算 e=∑(3−4k^2/(2k+1)!); k=0..10000
但是我卡住了,无法使用多线程获得所需的性能提升。
给定线程数,我尝试将总和分成 k / numberOfThreads
块,并为每个部分总和提交期货。
我认为不好的部分可能是阶乘计算或粒度。我尝试了一个较小的步骤,但没有得到很大的改进。也许需要一种不同的方法。
ExecutorService executor = Executors.newFixedThreadPool(numberOfThreads);
List<Future<BigDecimal>> futures = new ArrayList<>(numberOfThreads);
int step = k / numberOfThreads ;
BigDecimal result = BigDecimal.ZERO;
for (int j = 0; j <= k; j += step) {
Future<BigDecimal> future = executor.submit(new EulerCalculator(j, j + step));
futures.add(future);
}
for (Future<BigDecimal> future : futures) {
result = result.add(future.get());
}
public class EulerCalculator implements Callable<BigDecimal> {
private int start;
private int end;
public BigDecimal call() {
long numerator = 3 - 4 * start * start;
BigDecimal denominator = factorial(2 * start + 1);
BigDecimal partialSum = BigDecimal.valueOf(numerator)
.divide(denominator, 1000, RoundingMode.HALF_EVEN);
for (int i = start + 1 ; i < end; i++) {
numerator = 3 - 4 * i * i;
denominator = denominator.multiply(BigDecimal.valueOf(2 * i * (2*i + 1)));
partialSum = partialSum.add(BigDecimal.valueOf(numerator)
.divide(fact, 1000, RoundingMode.HALF_EVEN));
}
return partialSum;
}
private BigDecimal factorial(int cur) {
BigDecimal fact = BigDecimal.ONE;
for (int i = 2; i <= cur; i++) {
fact = fact.multiply(BigDecimal.valueOf(i));
}
return fact;
}
}
在四核上运行几次的最佳结果:
k = 10000
线程数 = 1:345 毫秒
线程数 = 2:216 毫秒
线程数 = 4:184 毫秒
线程数 = 8:225 毫秒
你的阶乘部分不是常数时间运算,而是 O(n)。这意味着您的第一个线程比最后一个线程的工作要少得多。因此你没有平均分配工作。
通常有三种方法可以解决这个问题。
你可以做不均匀的步长,即k越小步长越大。但是,这是非常低效的,因为您要进行数千次相同的乘法运算。
您可以尝试切换到近似算法来计算阶乘,使其达到常数时间。对于小 k,您可以使用迭代来防止精度损失,因为惩罚会很低,而且小 k 也不多。
另一种方法是构建一个大数组,其中包含所有可能用于计算的阶乘,在提交任何任务之前必须运行。这种缓存方法损失的精度较低。请参阅下面关于如何并行化此过程的评论。
由于您需要所有 denominator
,并且每个都依赖于之前的 ALL,因此我将有一个专用线程来计算所有这些;并且对于每个 denominator
计算提交一个不同的任务到你的线程池来并行计算特定的部分和。最后使用 parallel stream 聚合所有结果。以下代码显示了这些详细信息:
public static BigDecimal calculate(int k, int numberOfThreads) {
ExecutorService executor = Executors.newFixedThreadPool(numberOfThreads);
List<Future<BigDecimal>> futures = new ArrayList<>(numberOfThreads);
BigDecimal denominator = BigDecimal.ONE;
for (int j = 1; j <= k; j++) {
denominator = denominator.multiply(BigDecimal.valueOf(4 * j * j + 2 * j));
Future<BigDecimal> future = executor.submit(computePartialSum(j, denominator));
futures.add(future);
}
return futures.stream().parallel()
.map(future.get())
.reduce(BigDecimal.ZERO, BigDecimal::add).add(BigDecimal.valueOf(3));
}
public static Callable<BigDecimal> computePartialSum(int curr, BigDecimal denominator) {
return () -> {
long numerator = 3 - 4 * curr * curr;
return BigDecimal.valueOf(numerator).divide(denominator, 1000, RoundingMode.HALF_EVEN);
};
}
不过,您的瓶颈将是阶乘的计算;您可以将其划分为更小的阶乘段并缓存它们以聚合成它们的真实值,我的两分钱。
感谢您的回答!
我用一个简单的 for
循环缓存了阶乘,我得到了另一个计算的好结果:
1 thread = 17ms
2 threads = 10ms
4 threads = 7ms
但是我需要画一个类似于下图的图表,只有我利用线程计算阶乘才有可能。
我测试了这个n!
算法:
public BigDecimal calculate(int number) {
if (number == 0 || number == 1) {
return BigDecimal.ONE;
}
List<Callable<BigDecimal>> callables = new ArrayList<>();
int step = number / processors;
for (int i = 2; i <= number; i += step + 1) {
callables.add(new FactorialPartCalculator(i, i + step >= number ? number : i + step));
}
List<Future<BigDecimal>> futures = executor.invokeAll(callables);
BigDecimal result = BigDecimal.ONE;
for (Future<BigDecimal> future : futures) {
result = result.multiply(future.get());
}
return result;
}
public class FactorialPartCalculator implements Callable<BigDecimal> {
@Override
public BigDecimal call() throws Exception {
BigDecimal factorialPart = BigDecimal.ONE;
for (int i = start; i <= end; i++) {
factorialPart = factorialPart.multiply(BigDecimal.valueOf(i));
}
return factorialPart;
}
我在 20000!
的 6 个线程中获得了 6.4 倍的加速。
所以我需要缓存阶乘并将缓存过程包括在整体时间中。该程序将在 32 个处理器上进行测试,我应该获得尽可能多的加速
所以我的问题是如何更改上述算法以将所有阶乘存储在一个数组中?如果有帮助,我只需要奇数阶乘。