再帰は並行して実行できますか?それは理にかなっていますか?


8

たとえば、次のように実行されるfibonacciの単純な再帰的アルゴリズムを使用しているとします。

fib(5) -> fib(4)+fib(3)
            |      |
      fib(3)+fib(2)|
                fib(2)+fib(1)

等々

これで、実行は引き続きシーケンシャルになります。その代わりに、どのように私がいることを、このように、コードだろうfib(4)とはfib(3)、2つの別々のスレッドを生成することによって計算されるが、その後にfib(4)、2つのスレッドがために生み出されているfib(3)fib(2)。がいつfib(3)に分割されるのかfib(2)と同じfib(1)ですか?

(動的プログラミングはフィボナッチにとってはるかに優れたアプローチであることを知っています。ここで簡単な例として使用しただけです)

(誰かがC \ C ++ \ C#でもコードサンプルを共有できる場合、それは理想的です)


3
もちろん、これは可能であり、時にはそれが役立つことさえあります。唯一の条件は、それfibが純粋な関数であることです(おそらくここではそうです)。優れた特性は、順次再帰バージョンが正しい場合、並列バージョンも正しくなることです。しかし、それが正しくなく、再帰が無限に続く場合は、突然フォーク爆弾が作成されます。
アモン

この場合、スレッドプーリングは可能ですか?私はそれは計算がそのスレッドから、ではないと思うfib(n)、それは両方から結果を取得するまで終了しませんfib(n-1)fib(n-2)。スレッドが終了してポーリングに戻るには、プールから別のスレッドを取得する必要があるため、これによりデッドロックが発生します。これを回避する方法はありますか?
Idan Arye 2014年

1
あなたは見つけるかもしれないのMapReduceを使用して再帰的計算スタックオーバーフローに興味深い読み取りを。

回答:


27

これは可能ですが、非常に悪い考えです。たとえば、fib(16)を計算するときにスポーンするスレッドの数を計算し、それにスレッドのコストを掛けます。スレッドはめちゃくちゃ高価です。あなたが説明するタスクのためにこれを行うことは、小説の各キャラクターをタイプするために異なるタイピストを雇うようなものです。

とは言っても、再帰的アルゴリズムは、特にジョブを独立して実行できる2つの小さなジョブに分割する場合は、並列化の良い候補になることがよくあります。トリックは、並列化を停止するタイミングを知ることです。

一般に、「非常に並列」なタスクのみを並列化する必要があります。つまり、計算コストが高く独立して計算できるタスクです。多くの人は最初の部分を忘れています。スレッドには、それだけであなたが持っているとき、ものを作ることは理にかなっていることをとても高価で巨大な彼らが行うための作業量を、しかも、あなたがスレッドにプロセッサ全体を捧げることができること。8つのプロセッサがある場合、80のスレッドを作成すると、プロセッサを共有するように強制され、各スレッドの速度が大幅に低下します。8つのスレッドのみを作成し、非常に並列処理が必要なタスクがある場合は、それぞれがプロセッサに100%アクセスできるようにすることをお勧めします。

.NETのTask Parallel Libraryなどのライブラリは、並列処理がどれだけ効率的かを自動的に判断するように設計されています。このテーマに興味がある場合は、そのデザインを調査することを検討してください。


3

質問には実際には2つの答えがあります。

再帰は並行して実行できますか?それは理にかなっていますか?

はい、もちろん。ほとんどの(すべての?)ケースで、再帰的アルゴリズムは再帰なしで書き直すことができ、非常に頻繁に並列化可能なアルゴリズムにつながります。常にではないが、頻繁に。

クイックソート、またはディレクトリツリーを反復処理することを考えてください。どちらの場合も、キューを使用して、すべての中間結果を保持できます。サブディレクトリが見つかりました。キューは並行して処理でき、タスクが正常に完了するまで、最終的にはさらにエントリを作成します。

何についてfib()の例?

残念ながら、入力値の完全性は以前に計算された結果に依存するため、フィボナッチ関数は不適切な選択です。この依存関係により、毎回とを使用して並列処理を行うことが困難に1なり1ます。

ただし、フィボナッチ計算をより頻繁に行う必要がある場合は、その時点までのすべての計算を回避するために、事前計算された結果を保存(またはキャッシュ)することをお勧めします。背後にあるコンセプトは、レインボーテーブルに非常に似ています。

たとえば、Fibo番号のペアを10個ごとに最大10.000までキャッシュするとします。この初期化ルーチンをバックグラウンドスレッドで開始します。これで、誰かがフィボ番号5246を要求した場合、アルゴリズムは5240からペアを取得し、そのポイントから計算を開始します。5240ペアがまだない場合は、そのままお待ちください。

この方法では、ランダムに選択された多くのフィボ数の計算を非常に効率的に並行して実行できます。これは、2つのスレッドが同じ数を計算しなければならないことはほとんどないためです。それでも、それほど問題にはなりません。


1

もちろん、それは可能ですが、そのような小さな例(実際には、はるかに大きなものの多く)では、記述しなければならない配管/同時実行制御コードの量により、ビジネスコードがわかりにくくなるほど、あいまいになります。あなたが本当に、本当に、本当に非常に高速に計算されたフィボナッチ数が必要でない限り、良い考えです。

アルゴリズムを通常どおりに公式化し、TBBGCDなどの同時実行ライブラリ/言語拡張で実際にステップをスレッドに分散する方法を処理する方が、ほとんどの場合より読みやすく、保守しやすくなります。


0

あなたの例では、fib(3)を2回計算しているため、fib(1)とfib(2)全体が2回実行されます。

おそらく非再帰的なソリューションよりも速度は向上しますが、リソース(プロセッサ)の価値はそれよりもはるかに高くなります。


0

はい、できます!私があなたに与えることができる私の最も簡単な例は、数値の二分木を想像することです。なんらかの理由で、バイナリツリーのすべての数値を合計したいとします。まあそうするために、あなたは、左/右のノードの値にルート・ノードの値を追加する必要が....しかし、ノード自体は別のツリー(元ツリーへのサブツリー)のルートかもしれ
代わりに計算します左側のサブツリーの合計、次に右側の合計...次にルートの値に追加します...左側と右側のサブツリーの合計を並列で計算できます。


0

1つの問題は、fib(n)を計算するための呼び出しの数がfib(n)と等しいため、fibonacci関数の標準の再帰アルゴリズムが非常に悪いことです。だから私は本当にそれを議論することを拒否します。

より合理的な再帰アルゴリズムであるQuicksortを見てみましょう。次のようにして配列をソートします。配列が小さい場合は、バブルソート、挿入ソートなどを使用してソートします。それ以外の場合:配列の1つの要素を選択します。小さい要素はすべて片側に、大きい要素はすべて反対側に配置します。小さい要素のある側を並べ替えます。要素が大きい側を並べ替えます。

恣意的に深い再帰を回避するために、通常の方法は、クイックソート関数が2つの小さい方(要素が少ない方)に対して再帰呼び出しを行い、大きい方自体を処理します。

これで、複数のスレッドを使用する非常に簡単な方法があります。小さい側をソートするために再帰呼び出しを行う代わりに、スレッドを開始します。次に、大きい方の半分を並べ替え、スレッドが終了するまで待ちます。ただし、スレッドの開始にはコストがかかります。したがって、スレッドを作成する時間と比較して、n要素をソートするのに平均してどれくらい時間がかかるかを測定します。それから、新しいスレッドを作成する価値があるような最小のnを見つけます。したがって、ソートが必要な小さい方がそのサイズよりも小さい場合は、再帰呼び出しを行います。それ以外の場合は、その半分を新しいスレッドで並べ替えます。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.