web-dev-qa-db-ja.com

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

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

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

等々

これで、実行は引き続きシーケンシャルになります。その代わりに、fib(4)fib(3)が2つの別々のスレッドを生成することによって計算されるように、これをどのようにコード化しますか。次にfib(4)で、fib(3)およびfib(2)fib(3)fib(2)fib(1)に分割されている場合も同じですか?

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

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

8
Akash

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

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

一般に、「恥ずかしいほど並列」なタスクのみを並列化する必要があります。つまり、計算コストが高く、独立して計算できるタスクです。多くの人は最初の部分を忘れています。スレッドは非常に高価であるため、hugeの量の作業があり、さらにプロセッサ全体をスレッドに割り当てることができます。 8つのプロセッサがある場合、80個のスレッドを作成すると、スレッドがプロセッサを共有するように強制され、それぞれのスレッドが大幅に遅くなります。 8つのスレッドのみを作成し、非常に並列なタスクを実行する場合は、スレッドごとにプロセッサへの100%のアクセスを許可することをお勧めします。

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

26
Eric Lippert

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

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

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

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

fib()の例はどうですか?

残念ながら、入力値の完全性は以前に計算された結果に依存するため、フィボナッチ関数は悪い選択です。この依存関係により、毎回1および1で開始する場合、並行して実行することが困難になります。

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

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

この方法では、2つのスレッドが同じ数を計算する必要がほとんどないため、ランダムに選択された多数のfibo数の計算を非常に効率的かつ並列に実行できます。それでも、それほど問題にはなりません。

3
JensG

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

ほとんどの場合、通常どおりにアルゴリズムを作成し、次に [〜#〜] tbb [〜#〜] または [〜#〜 ] gcd [〜#〜] 実際にステップをスレッドに分散する方法に注意してください。

1
Kilian Foth

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

0
Vincent Grigori

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

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

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

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

0
gnasher729

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

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

0
spado