Elliott правильно в своем комментарии, что многопоточность не является хорошим способом, чтобы оптимизировать вычисление факториалов. Конечно, это будет неэффективно (по сравнению с воспоминаниями, например), пока вы не вычислите действительно большие факториалы.
В простой рекурсивной разделяй и властвуй стратегии, псевдо-код
factorial(N) =
return factorialHelper(set_of(1, N))
factorialHelper(set) =
if set.size == 1
return set.first
else
let set1, set2 = split_into_subsets(set, 2)
return factorialHelper(set1) * factorialHelper(set2)
Теперь мы можем наивности использовать потоки, как это:
factorialHelper(set) =
if set.size == 1
return set.first
else
let set1, set2 = split_into_subsets(set, 2)
return fork(lambda factorialHelper(set1)).join() *
fork(lambda factorialHelper(set2)).join()
(Пояснение: fork(lambda factorialHelper(set1)).join()
должно означать, что мы : - создание новой темы для расчета factorialHelper(set1)
, - начало темы, - ждет, пока сообщение не получено.)
В этом проблема. Каждый поток выполняет кучу домашнего хозяйства (например, разделение набора), а затем одно умножение. И если вы посмотрите на общую картину, это означает, что N! требуется примерно N потоков.
Мы можем сделать лучше. Вместо того, чтобы ждать завершения двух субтитров. «Текущая» нить могла выполнять работу одного из них; например
factorialHelper(set) =
if set.size == 1
return set.first
else
let set1, set2 = split_into_subsets(set, 2)
let child = fork(lanbda factorialHelper(set1))
return factorialHelper(set2) * child.join()
Для этого требуется примерно N/2 резьбы. И есть большие проблемы:
- Создание потоков очень дорого.
- Резьбовое переключение/ожидание завершения другой резьбы относительно дорого.
- В любой момент времени на вашем компьютере будет работать столько потоков, сколько у него есть физические (или гипертекстовые) ядра.
Так что, если бы вы хотели кодировать выше буквально на Java, он работал бы как собака. Слишком много потоков. Слишком много создания потоков/переключения накладных расходов и слишком мало полезно Работа для каждой нити.
Если вы используете Java Fork/Join Pool, вы можете сделать намного лучше, но вам также нужно что-то сделать о шаге split_into_subsets(set, 2)
. Это собирается сделать много лишней работы копирование набор элементов.
Классический подход состоит в том, чтобы иметь один общий массив (от 1 до N) и передавать «низкие» и «высокие» индексы. Но в этом случае нам даже не нужен массив, потому что мы знаем, что array[i]
- это то же самое, что и i
. (Игнорировать по-одному. Я все еще говорю псевдокод!)
Но тогда мы дойдем до последней проблемы. Балансировка рабочей нагрузки. Работа умножения двух больших (например, BigInteger
) чисел не является постоянной. Фактически это зависит от величины чисел. (Я думаю, что для M * N
это O (журнал M
* войти N
). Если вы используете наивные разделяй и властвуй, умножений на «левом конце» гораздо быстрее, чем у «правого конца». Решая это сложно, и я подозреваю, что разделяй и властвуй неправильный подход для работы с ним
Я бы на самом деле рассмотреть гибридный подход:.
- Создать
T
рабочие потоки, где T
соответствует числу доступных ядер.
- Распределите номера
1
через N
в T
подмножества, в круговой форме.
- Получите каждую нить (последовательно) вычислить произведение всех чисел в своем подмножестве.
- Сделайте окончательные умножения продуктов подмножества, используя как можно больше потоков.
В конце расчета, независимо от того, как вы это сделаете, произойдет окончательное умножение, которое будет выполнено с использованием одного потока. И два вычисления до этого могут быть выполнены не более чем на 2 потока. Так что в конце концов, какой-то поток «голода» будет неизбежен.
И наконец, существует само умножение. Ясно, что если мы вычисляем factorial (N) для достаточно большого N для нескольких потоков, чтобы стоить, результат будет больше Long.MAX_VALUE. Использование BigInteger
было бы очевидным выбором. Тем не менее, есть немного вопросительного знака над BigInteger.multiply(BigInteger)
. Возможно, стоит посмотреть на высокопроизводительные алгоритмы умножения N-разрядных чисел и алгоритмы умножения, которые можно распараллелить. Начало здесь:
Для записи, сложность BigInteger.multiply(BigInteger)
для двух п-значных чисел следующим образом:
Java 6 использует алгоритм "школьный мальчик" ; т.е. сложность O (n * n) для n цифр
Java 8 использует алгоритм «школьного мальчика», алгоритм Карацубы (O (n * 1.585)) или трехстороннее умножение Toom-Cook (O (n * 1.465)) в зависимости от размера n. Таким образом, в конечном счете, сложность O (n * 1.465).
Я не думаю, что это будет работайте так, как вы ожидаете, с несколькими потоками, и без нескольких потоков это [memoization] (https://en.wikipedia.org/wiki/Memoizati на). –
@ ElliottFrisch вы можете объяснить самый быстрый способ решить это, а не традиционный способ использования рекурсии ... спасибо за комментарий – SmashCode
Никто не хочет делать домашнее задание для вас. – jdv