Previous Contents Next
Chapter 6 A propos des récurrences
Les récurrences sont très utilisées dans les programmes numériques. Presque tous les programmes comportent des sommations d'éléments d'un tableau, des calculs de produits scalaires, des calculs d'extrema, etc. Des récurrences plus complexes sont parfois utilisées comme la recherche d'un élément particulier dans un tableau ou le calcul des moyennes des éléments en ligne et en colonne d'une matrice. Or ces récurrences sont, le plus souvent, mal parallélisées par les compilateurs basés sur la méthode géométrique. Ce chapitre explique pourquoi et quels remèdes peuvent être appliqués.

6.1 Les récurrences et la parallélisation automatique
Il y a du parallélisme implicite dans un programme dès que l'ordre d'exécution de certaines de ses opérations peut être changé sans modifier son résultat final. Ce genre de commutativité peut avoir deux sources différentes. Par exemple le calcul de la somme
s=
n
å
i=1
x(i)
est usuellement implanté par les quelques lignes ci-dessous.
INTEGER i,n
REAL s,x(1000)
s=0.
DO i=1,n
 s=s+x(i)
END DO
Un paralléliseur ordinaire 1 déclare que cette boucle est séquentielle à cause de la dépendance PC sur s. En conséquence, même sur une machine parallèle cette boucle s'exécutera en un temps de l'ordre de n. Cependant comme l'addition est associative et commutative 2, le calcul de la somme peut être fractionné en plusieurs sous-sommes portant sur n/p éléments (où p est le nombre de processeurs). Il suffit ensuite de cumuler les sommes partielles. Le temps d'exécution du calcul est donc de l'ordre de n/p+log2p.

Il est donc important pour un compilateur d'exploiter le parallélisme implicite des calculs tels que (6.1). Deux étapes sont nécessaires. La première étape consiste à repérer toutes les récurrences. Comme la plupart des langages ne disposent pas de notations pour désigner des calculs comme (6.1), les récurrences sont exprimées à l'aide de boucles. Il faut donc reconnaître les boucles utilisées pour calculer des récurrences. Cela devient compliqué pour des programmes comportant des boucles emboîtées et manipulant des tableaux (surtout des tableaux multi-dimensionnels). La seconde étape consiste à vérifier que la récurrence est efficacement implantable sur une machine parallèle. Ceci revient à vérifier que la fonction associée à la récurrence respecte certaines propriétés. Actuellement, l'unique moyen de réaliser ce test est d'utiliser une reconnaissance de forme. L'efficacité de la reconnaissance de forme, en terme de rapidité d'exécution, est liée à la taille de la base de règles. D'où l'intérêt de réduire cette taille au minimum en normalisant les récurrences, c'est-à-dire en normalisant le programme source. En fait notre but est de chercher à détecter uniquement des récurrences simples (i.e. des récurrences définies par une seule instruction d'affectation). Il faut donc savoir dans quelle mesure les récurrences d'un programme complexe peuvent être ramenées à des récurrences simples par normalisation du programme.

Pour mener à bien ce travail, il faut connaître précisément les récurrences qui peuvent s'exécuter sur une machine parallèle. Il est aussi nécessaire de définir la notion de récurrence dans un programme écrit dans un langage impératif. Nous verrons que cela conduit à la fois à étendre la notion de récurrence (e.g. comme nous manipulons des tableaux de valeurs, nous introduirons le concept de récurrence multi-directionnelle) et à la réduire (e.g. nous imposons des contraintes drastiques sur la fonction associée à la récurrence).

6.2 Parallélisation de récurrences
Pour l'instant nous ne cherchons pas à préciser la notion de récurrence dans un programme impératif, mais juste à caractériser les différents calculs, regroupés sous le nom de calculs de récurrence, qui peuvent être exécutés plus vite sur une machine parallèle que sur une machine séquentielle.

Les récurrences qui peuvent être calculées efficacement sur des machines parallèles sont celles qui peuvent être implantées à l'aide d'un opérateur Ä binaire et associatif. La récurrence s'écrit :
é
ë
s(0) = a(0)
" iÎ Nn*, s(i) = s(i-1)Ä a(i)
(La notation Nn* est utilisée pour représenter l'ensemble des entiers naturels de 1 à n, la variante non étoilée Nn est égale à Nn*È {0}). Il est à noter qu'aucune contrainte n'impose que Ä soit une opération sur des scalaires. Par exemple, un produit de matrice est lui aussi associatif et donc convient. Considérons d'abord le cas où seule la valeur finale de la récurrence est utile. Ce type de récurrence est appelée une réduction 3. La valeur finale peut être calculée par un programme impératif du type :
s=a(0)
DO i=1,n
  s=s
Ä a(i)
END DO
Les opérations sont alors effectuées comme indiquées sur le schéma ci-dessous.
Dans ce cas de figure une seule opération peut être exécutée à la fois. En fait, il est possible, grâce à l'associativité de Ä de réordonner les opérations suivant un arbre binaire de calcul comme montré sur le schéma suivant :
Cette fois plusieurs opérations peuvent être exécutées en parallèle. Sur une machine avec un nombre illimité de processeurs, il est possible d'effectuer le calcul en log2(n) étapes. Pour une machine réelle, les n données sont réparties sur les p processeurs et seule l'étape finale de traitement des résultats partiels s'effectue suivant un arbre binaire de calcul. Le temps d'exécution est alors de n/p+log2(p). Si p est petit et que la machine parallèle utilisée n'est pas adaptée au parallélisme à grain fin, il est avantageux d'implanter en séquentiel l'étape finale. En effet, le surcoût de la distribution du travail aux p processeurs masque alors le gain dû à la parallélisation. Le temps d'exécution est alors de l'ordre de n/p+p.

Dans le cas où toutes les valeurs de la récurrence sont utiles la récurrence est appelée un scan 4. Un programme séquentiel pour calculer le scan est :
s(0)=a(0)
DO i=1,n
  s(i)=s(i-1)
Ä a(i)
END DO
Il est possible de calculer un scan en parallèle. Une première passe utilise le même arbre binaire de calcul que celui de la réduction. La seconde passe effectue une remontée dans l'arbre pour mettre à jour les résultats partiels (pour plus de détails voir par exemple [12]). Sur une machine avec p processeurs la complexité du calcul du scan est toujours de l'ordre de n/p+log2(p). Par contre comme deux parcours de l'arbre binaire sont nécessaires, le temps de calcul d'un scan est deux fois plus grand que celui d'une réduction. Dans la suite de cette thèse les mots réduction et scan désigneront toujours des récurrences associées à des opérateurs associatifs, c'est-à-dire des récurrences calculables efficacement sur une machine parallèle.

La parallélisation de réductions et de scans ne fait pas l'unanimité dans le monde du calcul numérique. Le fait est que les additions et les multiplications informatiques ne sont pas vraiment associatives. La parallélisation des réductions et des scans peut donc conduire à des résultats numériques légèrement différents. Dans le cas de méthodes de calcul itératives, la différence de résultat peut conduire à un ralentissement de la convergence (voire à un programme ne convergeant plus). En fait, c'est plutôt le contraire qui risque de se passer. Il a été montré, à l'aide d'un modèle probabiliste [46] que le calcul d'une somme suivant un arbre binaire est, la plupart du temps, plus précis que le calcul séquentiel. Il n'est donc pas du tout évident que la parallélisation des réductions et des scans nuise à la convergence d'un calcul. Dans le cas où l'opération est l'addition, le calcul est même plus précis.

Certains travaux présentent des méthodes pour paralléliser des récurrences utilisant des fonctions non-associatives (e.g. [17]). Ces travaux se basent sur le fait que la composition des fonctions ° est, elle, associative. L'inconvénient est que ° est une opération très difficile à implanter et qui peut conduire à des tailles de fonctions croissant de façon exponentielle. La parallélisation est alors fictive car les opérations de base du programme parallèle ont une complexité beaucoup plus grande que celles du programme séquentiel.

6.3 Les définitions du terme récurrence
Tout d'abord un point de vocabulaire. Dans cette thèse il est constamment question de récurrences ; il faudrait parler, en toute rigueur, de ``suites récurrentes''. Cependant nous utiliserons le terme ``récurrences'' car il est désormais très largement répandu. Notre but est de détecter des récurrences dans des programmes numériques. Un point délicat est de savoir quelles récurrences il est nécessaire de traiter, ce qui revient à donner une définition de la notion de récurrence dans les programmes impératifs. Il est logique de chercher une définition mathématique des récurrences. Il est assez frappant de constater qu'il n'en existe pas qui réponde à nos besoins. Les récurrences sont utilisées dans de nombreuses disciplines mathématiques mais ne sont presque jamais formalisées. Quand elles le sont c'est dans le cadre d'une étude bien précise. Citons, par exemple, la définition d'une récurrence trouvée dans un cours de mathématiques ([35]). Dans cet ouvrage une récurrence est une suite (un)n³ 0 vérifiant :
" nÎ Nun+k=ak-1 un+k-1+ ··· + a0 un  ,
où les ai sont des complexes et a0 est non nul. Cette définition est trop restrictive. Seules les récurrences à base d'additions sont envisagées or nous avons vu que toutes les récurrences que l'on peut calculer à l'aide d'une opération binaire et associative peuvent être efficacement parallélisées. De plus l'espace de calcul de la récurrence est une suite au sens strict. C'est à dire, d'un point de vue informatique, un tableau uni-dimensionnel. Une récurrence, dans un programme, peut avoir un espace de calcul plus complexe à cause des tableaux multi-dimensionnels. En fait la définition la plus adaptée à notre problème est celle du lambda-calcul ([6]). Une récurrence y est décrite comme la donnée de deux lambda-expressions M et N et d'une fonction p. La première valeur de la récurrence, notée R 0 est la valeur de l'expression N. La valeur de rang n est donnée par la formule
R n=M n ( R p(n))  .
Notez que la fonction M, dite fonction de propagation, est aussi paramétrée par l'indice n. Cette définition peut être étendue aux récurrences d'ordre supérieur à 1. Une récurrence est alors la donnée d'un tuple (M,N1,...,Nk) et d'une fonction p. Les k premières valeurs de la récurrence sont les expressions N1 à Nk. Un terme de rang n supérieur ou égal à k est donné par la formule
R n=M n ( R p(n)) ( R p2(n)) ... ( R pk(n))  ,
où l'élévation à la puissance s'entend en terme de composition de fonctions. Cette définition est la formalisation de la définition du dictionnaire Larousse qui indique qu'une récurrence est une suite ``dont le terme général s'exprime à partir de termes le précédant''. C'est la fonction p qui joue le rôle de fonction prédécesseur. D'un point de vue informatique cette fonction prédécesseur associe à chaque terme de la récurrence la variable dans laquelle il doit être stocké. Il est évidemment impossible de détecter toutes les récurrences qui suivent cette définition. Pour s'en convaincre il suffit de considérer un programme quelconque P s'exécutant sur une machine donnée M. Ce programme peut être calculé par la récurrence telle que : C'est d'ailleurs cette récurrence qui est à la base de la sémantique dénotationelle [33] et de la sémantique opérationnelle. Il est donc sans objet de chercher à détecter toutes les récurrences. Seules doivent l'être les récurrences qui peuvent être efficacement parallélisées, c'est-à-dire celles présentées dans la section 6.2 et définies par le système (6.2). Ceci impose donc qu'une fonction binaire et associative puisse être extraite de la fonction de propagation de la récurrence. De plus seules les fonctions d'ordre 1 seront considérées. Il est par contre possible de transformer certaines récurrences d'ordre supérieur en récurrences d'ordre 1.
Considérons par exemple la récurrence d'ordre 2 ci-dessous.
é
ê
ê
ë
s(0) = 0
  s(1) = 1
" iÎ {2,...,n}, s(i) = a(i).s(i-1)+b(i).s(i-2)+c(i)
La récurrence calculant (S(i))iÎ {2,...,n}S(i) est le vecteur (
s(i)
s(i-1)
) est :
é
ê
ê
ë
  S(1) =
æ
è
1
0
ö
ø
" iÎ {2,...,n}, S(i) =
é
ë
a(i) b(i)
1 0
ù
û
S(i-1)+ æ
è
c(i)
0
ö
ø
Cette dernière récurrence est bien d'ordre 1 et donc peut être parallélisée. La parallélisation n'est cependant pas aussi efficace que celle d'une récurrence d'ordre 1. En effet chaque terme est calculé deux fois, une fois dans S(i) et une fois dans S(i+1). Ainsi, deux fois plus de processeurs sont nécessaires pour calculer en parallèle une récurrence d'ordre 2, du moins pour obtenir le même temps d'exécution qu'avec une récurrence d'ordre 1. L'article [39] présente une méthode pour réduire ce nombre de processeurs. Le procédé présenté ici s'étend aux récurrences d'ordre o.
Enfin, une autre caractéristique de la récurrence (6.2) est que sa fonction prédécesseur (i.e. i|® i-1 sur N) est bijective. Les récurrences possédant des fonctions prédécesseur non bijectives sont donc écartées.
Considérons la récurrence ci-dessous :
DO i=1,n
 DO j=1,n
  s(i,j)=s(i,j-1)+s(i-1,j)+s(i,j)
 END DO
END DO
Le graphe des dépendances du programme est illustré par le schéma suivant :
La fonction prédécesseur de cette récurrence est :
p(i,j)={ (i,j-1), (i-1,j) }  .
Il ne s'agit pas d'une fonction bijective, cette récurrence ne peut s'exprimer à l'aide d'un système tel que (6.2).
Même avec ces restrictions le problème est encore trop compliqué. En effet, sans autre hypothèse, toute sous-récurrence de la récurrence définissant le programme entier et possédant une fonction prédécesseur bijective convient. Dans le cas général, la fonction prédécesseur opère donc sur les opérations du programme. Dans la section 5.3 il a été vu que ces opérations sont de la forme (i,v), avec i le label d'une instruction et v un vecteur d'itération. Les récurrences portant sur plusieurs instructions (donc potentiellement sur différents tableaux) seraient donc admises.
Il est possible de tirer directement une récurrence du programme suivant :
t(0)=0
DO i=1,n
 s(i)=t(i-1)+a(2*i-1)       (v1)
 t(i)=s(i)+a(2*i)           (v2)
END DO
Les labels v1 et v2 sont les labels de la seconde et de la troisième instructions d'affectation du programme. Les termes de la récurrence sont indicés par les opérations (v1,i)iÎNn* et (v2,i)iÎNn*. La fonction prédécesseur est définie par cas :
" iÎ{2,...,n}, p((v1,i))=(v2,i-1)  et,
" iÎ{1,...,n}, p((v2,i))=(v1,i)  .
La fonction de propagation est elle aussi définie par cas :
" iÎ{2,...,n}, M((v1,i),x)=x+a(2i-1)  et,
" iÎ{1,...,n}, M((v2,i),x)=x+a(2i)  .
Il est bien trop coûteux de détecter ce genre de récurrence automatiquement.
Des récurrences calculées suivant des trajectoires les plus bizarres seraient aussi admises.
Le programme suivant représente une récurrence :
s(0)=0
DO i=1,n
 s(i*i)=s((i-1)*(i-1))+a(i)   (v1)
END DO
La fonction prédécesseur est :
" iÎ{2,...,n}, p((v1,i))= (v1 ,(i
1
2
 
-1)2)  .
La fonction de propagation s'écrit :
" iÎ{2,...,n}, M((v1,i),x)=x+a(i)  .
Le problème est que la fonction de propagation n'est pas manipulable avec les outils de l'algèbre linéaire.
Il n'est pas réaliste d'essayer de détecter des récurrences avec de telles fonctions de propagation. Dans le cas de récurrences sur plusieurs instructions cela nécessiterait d'appliquer une reconnaissance de forme sur tous les sous-ensembles des corps de boucles. La combinatoire est trop élevée surtout sachant que chaque opération élémentaire (une reconnaissance de forme) a un coût exponentiel. Comme, de plus, les programmes que nous étudions ont des fonctions d'indices affines, la fonction prédécesseur est limitée à des expressions affines.

Par contre nous n'imposons pas que la fonction de propagation définisse un ordre < total sur les termes de la récurrence. Ceci élargit la définition classique de récurrence. En fait, cela permet de décrire un ensemble de récurrences. Soit º<| la relation d'équivalence qui indique si deux termes de la récurrence sont comparables par <. Une récurrence classique est associée à chaque classe d'équivalence du quotient de l'ensemble des termes par º<|.
Deux récurrences sont définies par le programme ci-dessous.
DO i=2,n
  a(i)=a(i-2)+a(i)  (v1)
END DO
En effet deux classes d'équivalence sont définies (v1,i)i pair et (v1,i)i impair. Dans le même ordre d'idée, n récurrences sont définies par le programme :
DO i=1,n
  DO j=1,n
    a(i,j)=a(i-1,j)+a(i,j)   (v1)
  END DO
END DO
Les classes d'équivalence sont paramétrées par j : ce sont les (v1,(i,j))iÎNn*.
En résumé, il est raisonnable de détecter, par reconnaissance de forme, des récurrences : Comme de plus, dans le cadre de cette étude, seuls les scans ou les réductions nous intéressent il faut se limiter aux récurrences telles qu'une fonction binaire et associative peut être extraite de la fonction de propagation. Par contre aucune restriction n'est faite sur le domaine de définition des indices des termes de la récurrence. Il peut s'agir d'un espace à plusieurs dimensions (contrairement à la définition classique des récurrences). En outre, la définition de récurrence est élargie pour permettre la description de récurrences paramétrées.

6.4 État de l'art de la détection de récurrences
Des travaux ont déjà été menés sur la détection de récurrences, ou plus exactement sur la détection de réductions. L'article [37] présente une méthode basée sur une exécution symbolique (``symbolic stores'') du corps des boucles du programme. Dans [47] une méthode s'appuyant sur le graphe des dépendances est envisagée. Enfin l'article de Callahan [14] décrit une transformation de programmes adaptée à la parallélisation de récurrences quelconques.

6.4.1 Méthode basée sur l'exécution symbolique
L'évaluation symbolique est un procédé qui permet de résumer l'effet d'un ensemble d'instructions d'affectation. Cela revient à remplacer plusieurs instructions d'affectation par une seule instruction d'affectation multiple.
L'évaluation symbolique des trois affectations
x=z+2
t=2*x+z
z=sqrt(t)+x
donne pour la variable x la valeur z+2, pour la variable t la valeur 2*(z+2)+z et pour la variable z la valeur sqrt(2*(z+2)+z)+z+2. Il faut noter que les valeurs contiennent encore des variables d'où le terme d'évaluation symbolique ; les expressions sont manipulées comme des chaînes de caractères.
L'évaluation symbolique peut être étendue pour traiter les instructions conditionnelles (par exemple en transformant ces instructions en expressions conditionnelles).

La première étape de la méthode de détection de réductions par évaluation symbolique est justement l'évaluation symbolique des corps des boucles les plus internes. Une reconnaissance de forme est alors appliquée sur les valeurs symboliques obtenues pour reconnaître les réductions. Les nids de boucles peuvent être traités par propagation des réductions détectées.

Le principal avantage de cette méthode est que le programme est normalisé durant l'évaluation symbolique. Ainsi le résultat de la détection est relativement indépendant des différentes manières dont un algorithme peut être implanté (une simplification des valeurs symboliques doit tout de même être effectuée).
L'évaluation symbolique du corps de la boucle
s=0
DO i=1,n
  t=s+a(i)
  s=t+b(i)
END DO
donne pour la variable s la valeur s+a(i)+b(i). Une reconnaissance de forme peut alors s'apercevoir que s contient, en fin de boucle, la somme des a(i)+b(i).
Par contre un inconvénient de l'exécution symbolique d'un programme est que les tableaux ne sont pas correctement pris en compte. Par exemple a(i) et a(j+1) sont des chaînes de caractères différentes mais peuvent représenter la même cellule mémoire si i est égal à j+1. De ce fait certaines réductions échappent au détecteur.
Dans l'exemple suivant
DO i=2,n
 a(i)=b(i-1)+a(i)
 b(i)=a(i-1)+b(i)
END DO
une analyse symbolique n'est pas capable de constater que si i³ 3 alors la valeur affectée à a(i) est :
a(i-2)+b(i-1)+a(i)
En conséquence la réduction ci-dessous n'est pas détectée :
DO i=3,n
 a(i)=a(i-2)+b(i-1)+a(i)
END DO
Comme expliqué dans la section 6.3, cette récurrence contient en fait deux récurrences, une pour les i pairs et une pour les i impairs.
De la même façon les réductions implantées avec plusieurs boucles échappent à la détection.
Une analyse symbolique du programme ci-dessous échoue à détecter la réduction qu'il contient.
DO i=1,n
 DO j=1,m
  s(j)=t(j)+a(i,j)
 END DO
 DO k=1,m
  t(k)=s(k)
 END DO
END DO
En effet s(k) et s(j) sont des symboles différents ; l'analyse symbolique ne peut pas se rendre compte que l'effet d'une itération de la boucle externe consiste à ajouter à chaque t(k) la valeur de a(i,k).
De plus l'imprécision due à l'analyse symbolique peut conduire à remplacer, à tort, une portion de programme par le calcul d'une réduction. Pour éviter ces erreurs un test est utilisé pour sélectionner les corps de boucles dans lesquelles la détection peut s'effectuer sans risque. Comme le problème est complexe, le test est souvent pessimiste, c'est-à-dire qu'il vérifie une condition suffisante mais pas nécessaire. En conséquence encore plus de réductions passeront entre les mailles de la détection.
Il est illégal de dire que les tableaux x et y du programme
DO i=2,n
 x(i)=x(i-1)+y(i-1)
 y(i)=y(i-1)+2*x(i-1)
END DO
peuvent être calculés par les deux réductions ci-dessous :
DO i=2,n
 x(i)=x(i-1)+y(i-1)
END DO
DO i=2,n
 y(i)=y(i-1)+2*x(i-1)
END DO
Un moyen de s'en rendre compte est d'analyser les dépendances du programme initial.
6.4.2 Méthode basée sur le graphe de dépendance
Pour pouvoir détecter des récurrences au vu du graphe de dépendance (GD), il faut trouver un moyen de représenter les boucles du programme dans ce graphe. Un tel moyen est décrit dans [47]. En premier lieu, les boucles sont déroulées jusqu'à obtenir une forme normale. Une boucle est en forme normale quand chacune de ses itérations utilise des éléments de tableaux et des variables scalaires assignés dans l'itération elle-même ou dans la précédente.
Par exemple, la forme normale de la boucle
DO i=1,n
 x(i)=x(i-2)+x(i)
END DO
est
DO i=1,n,2
 x(i)=x(i-2)+x(i)
 x(i+1)=x(i-1)+x(i+1)
END DO
Quand la forme normale est atteinte, un GD générique de la boucle est construit (i.e. un GD résumant la boucle). Puisque la boucle est en forme normale, l'union du GD de l'itération initiale avec celui d'une itération intermédiaire et avec celui de l'itération finale donne une image exacte de la boucle entière. Ensuite, une reconnaissance de forme est appliquée au GD de la boucle, et pour chaque sous graphe qui décrit une récurrence, une portion de code appropriée est générée. Une telle méthode permet une meilleure détection (e.g. détecte des récurrences croisées). Son principal inconvénient est l'absence d'une normalisation systématique. La normalisation du programme est sensée se faire par les transformations classiques (substitution de temporaires, expansion de scalaires, inversion de boucles, etc.). Le choix des transformations à effectuer et de l'ordre d'application est un problème ouvert ; actuellement seules des heuristiques sont connues. De plus comme la reconnaissance de forme s'applique au GD dans son ensemble la complexité temporelle de la détection augmente rapidement avec la taille du programme en entrée. Une dernière limitation est que la forme normale pour les boucles n'existe que dans le cas de dépendances uniformes.
Ainsi la boucle ci-dessous ne possède pas de forme normale.
DO i=1,n
 x(i)=x(i-k)+x(i)
END DO
En fait cette boucle doit être considérée comme étant le calcul de k récurrences.
6.4.3 Méthode basée sur une transformation de code
Cette méthode n'est pas limitée aux réductions mais s'applique aux récurrences en général. Il existe une limitation, cependant ; les fonctions de propagation des récurrences doivent être des fonctions affines. La première étape consiste à extraire les récurrences brutes du programme initial. L'auteur suppose que ces récurrences sont de la forme :
DO i=1,n
   
Ai1=fi1(Ai-11)
   
...
   
Air=fir(Ai-1r)
END DO
.
Une normalisation préliminaire du programme est donc sous-entendue. De plus cette forme ne prend en compte que des tableaux uni-dimensionnels, il n'est pas sûr que cette méthode puisse s'étendre facilement à des tableaux multi-dimensionnels. Dans une seconde étape une reconnaissance de forme très simple est utilisée pour vérifier que les fonctions (fik)kÎNr* sont bien affines en les variables (Aik)kÎNr*. La dernière étape est une transformation de code astucieuse de la boucle initiale. Sans entrer dans les détails, il suffit de remarquer que le vecteur (Ai1,...,Air) est donné par une récurrence matricielle d'ordre 1 (le procédé est identique à celui utilisé pour paralléliser la récurrence (6.3) dans la section 6.3). Comme le produit de matrices est associatif, du parallélisme peut être extrait. De plus pour réduire le coût de la méthode, il faut tenir compte du fait que la matrice utilisée est creuse, donc que certains calculs peuvent être évités. Le code final peut être exécuté en un temps O(n/p+p) avec p processeurs.
Par exemple, le programme
DO i=1,n
  a(i)=a(i-1)+2*b(i-1)
  b(i)=a(i-1)+x(i)
END DO
définit un système de récurrences affines. Donc il peut être automatiquement transformé en un programme parallèle équivalent (C est le nombre d'itérations à calculer sur un processeur, c'est-à-dire é n/p ù) :
DO ALL i=0,n,C
  A(0,i)=[0,1,0]
  B(0,i)=[0,0,1]
  DO k=1,C
    A(k,i)=A(k-1,i)+2*B(k-1,i)
    B(k,i)=A(k-1,i)+[x(i+k),0,0]
  END DO
END DO
DO i=C,n,C
  a(i)=A(C,i-C)*[1,a(i-C),b(i-C)]
  b(i)=B(C,i-C)*[1,a(i-C),b(i-C)]
END DO
DO ALL i=1,n,C
  DO k=1,C
    a(i+k)=A(k,i)*[1,a(i),b(i)]
    b(i+k)=B(k,i)*[1,a(i),b(i)]
  END DO
END DO
Notez que les opérations de base du programme parallèle sont maintenant des opérations sur les vecteurs. Les crochets ([ et ]) sont utilisés comme constructeurs de vecteurs.
Cette méthode présente l'avantage d'avoir une phase de reconnaissance de forme réduite à sa plus simple expression. En fait cette méthode est plus une transformation source à source qu'une véritable détection de récurrences. Ce manque d'analyse conduit à la génération d'un code avec un surcoût important.
Dans l'exemple précédant le programme parallèle exécute 23 opérations arithmétiques (additions et multiplications) dans une itération et le programme séquentiel seulement 3 pour le même travail. Aussi pour obtenir une accélération de 2 il faut, au minimum, 16 processeurs.
L'extraction de récurrences simples de systèmes récurrents complexes peut réduire ce surcoût. De plus la méthode ne donne aucune indication sur la manière d'extraire les récurrences brutes. En conclusion la méthode de Callahan pour paralléliser les récurrences affines et la méthode de détection de récurrences présentée dans cette thèse sont complémentaires. La méthode de Callahan peut être utilisée après notre phase de normalisation du programme initial. La phase de normalisation améliore l'efficacité du code généré puisqu'il éclate les systèmes récurrents complexes en systèmes plus simples.
1
Quelques compilateurs commerciaux peuvent reconnaître des formes classiques et donc compiler efficacement ce genre d'exemple.
2
En négligeant les erreurs d'arrondi.
3
Le terme vient du langage de programmation APL.
4
Il s'agit encore d'un terme lié au langage APL.

Previous Contents Next