После просмотра обновленного кода я сделал следующие наблюдения:
Во-первых, программа запускается за доли секунды. Это означает, что это микро-тест. Некоторые ключевые особенности Java затрудняют надежное выполнение микротестов. См. Как мне написать правильный микротест в Java? Например, если программа не выполняет достаточного количества повторений, компилятор «точно вовремя» не успевает приступить к скомпилируйте его в машинный код, и вы закончите тестированием интерпретатора. Кажется возможным, что в вашем случае компилятору JIT требуется больше времени для запуска при наличии нескольких потоков,
В качестве примера, чтобы ваша программа выполняла больше работы, я изменил точность BigDecimal
со 100 до 10,000. и добавил al oop вокруг основного метода. Время выполнения измерялось следующим образом:
1 поток:
Calculated in: 2803 milliseconds
Calculated in: 1116 milliseconds
Calculated in: 1040 milliseconds
Calculated in: 1066 milliseconds
Calculated in: 1036 milliseconds
2 потока:
Calculated in: 2354 milliseconds
Calculated in: 856 milliseconds
Calculated in: 624 milliseconds
Calculated in: 659 milliseconds
Calculated in: 664 milliseconds
4 потока:
Calculated in: 1961 milliseconds
Calculated in: 797 milliseconds
Calculated in: 623 milliseconds
Calculated in: 536 milliseconds
Calculated in: 497 milliseconds
Второе наблюдение заключается в том, что значительная часть рабочей нагрузки не получает выгоды от использования нескольких потоков: каждый поток вычисляет каждый факториал. Это означает, что ускорение не может быть линейным - как описано законом Амдала .
Итак, как мы можем получить результат без вычисления факториалов? Один из способов - это метод Хорнера. В качестве примера рассмотрим более простой ряд sum(1/k!)
, который также преобразуется в e
, но немного медленнее, чем ваш.
Допустим, вы хотите вычислить sum(1/k!)
до k = 100. С помощью метода Хорнера вы можете начните с конца и извлеките общие множители:
sum(1/k!, k=0..n) = 1/100! + 1/99! + 1/98! + ... + 1/1! + 1/0!
= ((... (((1/100 + 1)/99 + 1)/98 + ...)/2 + 1)/1 + 1
Посмотрите, как вы начинаете с 1, делите на 100 и прибавляете 1, делите на 99 и прибавляете 1, делите на 98 и складываете 1 и так далее? Это делает программу очень простой:
private static BigDecimal serialHornerMethod() {
BigDecimal accumulator = BigDecimal.ONE;
for (int k = 10000; k > 0; k--) {
BigDecimal divisor = new BigDecimal(k);
accumulator = accumulator.divide(divisor, 10000, RoundingMode.HALF_EVEN)
.add(BigDecimal.ONE);
}
return accumulator;
}
Хорошо, это последовательный метод, как вы заставите его использовать параллельный? Вот пример для двух потоков: сначала разделите ряд на четные и нечетные члены:
1/100! + 1/99! + 1/98! + 1/97! + ... + 1/1! + 1/0! =
(1/100! + 1/98! + ... + 1/0!) + (1/99! + 1/97! + ... + 1/1!)
Затем примените метод Хорнера как к четным, так и к нечетным членам:
1/100! + 1/98! + 1/96! + ... + 1/2! + 1/0! =
((((1/(100*99) + 1)/(98*97) + 1)/(96*95) + ...)/(2*1) + 1
and:
1/99! + 1/97! + 1/95! + ... + 1/3! + 1/1! =
((((1/(99*98) + 1)/(97*96) + 1)/(95*94) + ...)/(3*2) + 1
Это просто так же легко реализовать, как последовательный метод, и вы довольно близко подбираетесь к линейному ускорению, переходя от 1 до 2 потоков:
private static BigDecimal partialHornerMethod(int start) {
BigDecimal accumulator = BigDecimal.ONE;
for (int i = start; i > 0; i -= 2) {
int f = i * (i + 1);
BigDecimal divisor = new BigDecimal(f);
accumulator = accumulator.divide(divisor, 10000, RoundingMode.HALF_EVEN)
.add(BigDecimal.ONE);
}
return accumulator;
}
// Usage:
ExecutorService executorService = Executors.newFixedThreadPool(2);
Future<BigDecimal> submit = executorService.submit(() -> partialHornerMethod(10000));
Future<BigDecimal> submit1 = executorService.submit(() -> partialHornerMethod(9999));
BigDecimal result = submit1.get().add(submit.get());