web-dev-qa-db-fra.com

La récursion peut-elle être faite en parallèle? Cela aurait-il un sens?

Dis, j'utilise un simple algo récursif pour Fibonacci, qui serait exécuté comme suit:

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

etc

Maintenant, l'exécution sera toujours séquentielle. Au lieu de cela, comment puis-je coder cela de sorte que fib(4) et fib(3) sont calculées en accumulant 2 threads distincts, puis dans fib(4), 2 threads sont générés pour fib(3) et fib(2). Idem pour quand fib(3) est divisé en fib(2) et fib(1)?

(Je suis conscient que la programmation dynamique serait une approche bien meilleure pour Fibonacci, il suffit de l'utiliser comme un exemple facile ici)

(Si quelqu'un pouvait partager un échantillon de code dans C\C++\C # aussi, ce serait idéal)

8
Akash

C'est possible mais une très mauvaise idée; Travaillez le nombre de threads que vous allez frayer lors du calcul de la FIB (16), dites, puis multipliez que par le coût d'un fil. Les fils sont incroyablement chers; Faire cela pour la tâche que vous décrivez, c'est comme embaucher une dactylographie différente de taper chaque caractère d'un roman.

Cela dit, des algorithmes récursifs sont souvent de bons candidats à la parallélisation, en particulier s'ils divisaient le travail en deux emplois plus petits pouvant être effectués de manière indépendante. L'astuce est de savoir quand arrêter de paralléliser.

En général, vous souhaitez parallementer uniquement des tâches "parallèles embarrassantes". C'est-à-dire des tâches qui sont calculerellement coûteuses et peuvent être calculées indépendamment. Beaucoup de gens oublient la première partie. Les fils sont si coûteux qu'il n'a de sens que de faire un lorsque vous avez un énorme quantité de travail à faire, et de plus, que vous pouvez consacrer un processeur entier sur le fil. Si vous avez 8 processeurs, la fabrication de 80 threads va les forcer à partager le processeur, en ralentissement de chacun d'entre eux. Vous ferez mieux de ne faire que 8 threads et laissez chacun un accès à 100% au processeur lorsque vous avez une tâche parallèle embarrassante à effectuer.

Les bibliothèques telles que la bibliothèque parallèle de tâche dans .NET sont conçues pour déterminer automatiquement la quantité de parallélisme efficace; Vous pouvez envisager de rechercher sa conception si ce sujet vous intéresse.

26
Eric Lippert

La question a deux réponses, en fait.

La récursion peut-elle être faite en parallèle? Cela aurait-il un sens?

Oui bien sûr. Dans la plupart des cas (tous?), Un algorithme récursif peut être réécrit d'une manière sans récursion, conduisant à un algorithme assez facilement parallélisable. Pas toujours, mais souvent.

Pensez Quicksort, ou itérant à travers un arbre de répertoire. Dans les deux cas, une file d'attente pourrait être utilisée pour contenir tous les résultats intermédiaires resp. sous-répertoires trouvés. La file d'attente peut être traitée en parallèle, en créant finalement plus d'entrées jusqu'à ce que la tâche soit terminée avec succès.

Qu'en est-il de l'exemple fib()?

Malheureusement, la fonction Fibonacci est un mauvais choix, car les valeurs d'entrée susceptibles dépendent des résultats précédemment calculés. Cette dépendance rend difficile de le faire en parallèle si vous commencez à chaque fois avec 1 et 1.

Toutefois, si vous devez faire des calculs de Fibonacci plus souvent, cela pourrait être une bonne idée de stocker (ou de cache) des résultats pré-calculés afin d'éviter tout calcul jusqu'à ce point. Le concept derrière est assez similaire aux tables arc-en-ciel.

Disons que vous mettez en cache chaque 10ème paire de numéros Fibo jusqu'à 10.000. Démarrez cette routine d'initialisation sur un fil d'arrière-plan. Maintenant, si quelqu'un demande le numéro de Fibo 5246, l'algorithme choisit simplement la paire à partir de 5240 et commencez à calculer à partir de ce point vers l'avant. Si la paire 5240 n'est pas encore là, attendez-la.

De cette façon, le calcul de nombreux nombres de fibo choisis au hasard pourrait être effectué de manière très efficace et en parallèle, car il est très improbable que deux threads devraient calculer les mêmes numéros - et même alors, ce ne serait pas une grande difficulté.

3
JensG

Bien sûr, il est possible, mais pour un exemple aussi petit (et, en effet, pour beaucoup plus grand), la quantité de code de contrôle de la plomberie/de la concurrence que vous deviez écrire serait obscurcir le code d'entreprise au point qu'il ne serait pas Soyez une bonne idée que si vous avez vraiment, vraiment, vraiment besoin de chiffres de Fibonacci calculés très rapidement.

Il est presque toujours plus lisible et maintenu pour formuler votre algorithme normalement, puis laisser une bibliothèque/une extension de langue de concurrence telle que [~ # ~ # ~ ~] ou GCD Prenez soin de la manière de répartir les étapes des threads.

1
Kilian Foth

Un problème est que l'algorithme récursif standard de la fonction FIBONACCI est trop mauvais, car le nombre d'appels à calculer FIB (N) est égal à la FIB (N) qui est une croissance très rapide. Je refuserais donc vraiment de discuter de celui-là.

Regardons un algorithme récursif plus raisonnable, Quicksort. Il trie un tableau en procédant comme suit: Si le tableau est petit, triez-le à l'aide de BubblesTort, de tri de l'insertion ou de quelque chose. Sinon: Choisissez un élément de la matrice. Mettez tous les plus petits éléments d'un côté, tous les plus grands éléments de l'autre côté. Triez le côté avec les plus petits éléments. Triez le côté avec les plus grands éléments.

Pour éviter une récursion arbitraire profonde, la méthode habituelle est que la fonction de tri rapide fera un appel récursif pour les plus petits des deux côtés (celui avec moins d'éléments) et gérer le plus grand côté lui-même.

Maintenant, vous avez un moyen très simple d'utiliser plusieurs threads: au lieu de faire un appel récursif pour trier le plus petit côté, démarrez un fil; Puis triez la plus grande moitié, puis attendez que le fil finisse. Mais les threads de départ sont chers. Donc, vous mesurez combien de temps il faut en moyenne pour trier n éléments, par rapport au temps pour créer un fil. De cela, vous trouvez le plus petit n tel qu'il vaut la peine de créer un nouveau fil. Donc, si le côté plus petit qui doit être trié est inférieur à cette taille, vous effectuez un appel récursif. Sinon, vous triez la moitié dans un nouveau fil.

0
gnasher729

Dans votre exemple, vous calculez deux fois le FIB (3) qui conduit à une double exécution de l'ensemble de la fibère (1) et de la FIB (2), pour un chiffre plus élevé, c'est encore pire.

Vous gagnerez probablement de la vitesse sur la solution non radiophonique, mais cela coûtera beaucoup plus de ressources (processeurs) que ce qu'il vaut.

0
spado