• Aucun résultat trouvé

Fragmentation sémantique développée

3.7 Terminaison et correction

4.1.4 Fragmentation sémantique développée

Afin de repousser un peu plus les limites de notre critère sémantique, nous proposons ici deux variations. Si le critère a une justification convaincante, le développement proposé ici est plus discutable et tient bien plus de l’heuristique. Il est tout à fait possible de s’en tenir au critère initial, qui a le mérite de produire des fragmentations simples. Certains exemples de programmes ne pourront néanmoins être analysés qu’en adoptant les deux améliorations décrites ci-après.

Augmentation de la durée de vie des singletons. Les problèmes que nous venons d’abor-der ont un rapport avec la durée de vie des singletons dans la fragmentation. Lorsqu’on doit trai-ter une instruction, on extrait les cellules accédées pour les placer dans des fragments singletons. C’est le début de la vie d’un singleton. Plus tard, après avoir traité l’instruction, on réintégrera le fragment singleton dans un autre fragment plus grand. Mais on n’est pas forcé de le faire directement après l’instruction traitée, on peut retarder la réintégration du singleton, augmentant effectivement sa durée de vie.

Si la durée de vie du singleton est trop courte, il y a un risque. On peut un peu plus loin dans le programme réutiliser le même singleton. Le singleton sera une nouvelle fois singularisé mais il aura perdu toutes ses propriétés individuelles entre temps. Il faut autant que possible que le singleton reste en vie tant qu’on continue à le manipuler.

On ne peut évidemment pas augmenter la durée de vie d’un singleton indéfiniment. Plus on augmente sa durée de vie, plus la variable de synthèse associée reste longtemps. Ceci se répercute sur le coût des opérateurs du domaine abstrait. À l’extrême, on garderait indéfiniment chaque singleton mais cela serait en contradiction avec le rôle des critères de fragmentation : composer un nombre borné de fragments.

Afin de trouver un compromis, nous nous proposons de conserver le singleton jusqu’à l’exé-cution suivante de l’instruction qui a nécessité son introduction. Ainsi, le nombre de fragments reste borné par le nombre d’accès et on maximise sous cette contrainte la durée de vie des sin-gletons.

En terme d’instrumentation, cela revient à retarder la composition de sorte qu’elle ait lieu juste avant la singularisation suivante. La réinitialisation devient inutile et on peut la retirer. Voici le résultat pour l’exemple de la section précédente.

Pouri de1à nfaire a1 ∪:a01 a01 :{A[i]} x ← A[i] a2 ∪:a02 a02 :{A[i]} y ← A[i] b∪:b0 b0 ::{B[i]} B[i]=x−y

Pour le traitement des tests nous pouvons faire persister deux fragments par accès. Le premier pour les tests réussis, le second pour les tests non réussis. Lorsque le test est satisfait, le premier sera le singleton de l’élément accédé tandis que le second sera vide. Lorsque le test n’est pas satisfait, c’est l’inverse. Avant le test, chacun de ces deux fragment sera composé respectivement avec ceux regroupant toutes les cellules ayant validé le test et toutes celles de ne l’ayant pas validé. Voici ce que donnerait notre exemple de test ainsi instrumenté.

a1 ∪:a01 a2 ∪:a02 atmp :{A[i]} SiA[i]<0alors a01 :{A[i]},a02 :∅,atmp:∅ j ← i sinon a01 :∅,a02:{A[i]},atmp:∅

Traitement des boucles imbriquées Une seconde idée, inspirée de [HP08] permet d’amé-liorer la précision des analyses en présence de boucles imbriquées. Il peut être utile au moins dans deux cas d’essayer de distinguer l’effet des différents niveaux de boucles.

Ceci est intéressant lorsqu’un algorithme itère plusieurs fois sur les mêmes cellules. Considé-rons le programme suivant, peu représentatif mais illustratif.

Pour j de1à nfaire

A[j] ← 0

Pouri de1à nfaire

Pour j de1à nfaire

A[j] ← A[j]+1

Si on s’arrête au critère simplifié, dès la fin de la première itération de la seconde boucle on aura mis dans le même fragment toutes les cellules du tableau. Au milieu d’une itération de la boucle interne il sera impossible de séparer les cellules déjà incrémentées de celles qui ne le sont

pas encore.

Pour traiter ce problème, nous pouvons développer l’idée à l’origine de notre critère de frag-mentation. Cette idée était qu’une instruction produit les même effets d’une itération à l’autre. Mais dans l’exemple précédent, on peut considérer que l’effet de l’affectation varie selon l’ité-ration de la boucle externe. Il est vrai que à chaque itél’ité-ration de la boucle interne, l’effet est le même, la même valeur est associée à chaque cellule. Mais pour une autre itération de la boucle externe, la valeur affectée sera différente. Il ne serait donc pas tout à fait logique de chercher à abstraire de la même manière une exécution de l’affectation pour deux itérations distinctes de la boucle externe. On pourrait donc tenter de distinguer à nouveau les cellules suivant l’itéra-tion de la boucle externe à laquelle elles ont été modifiées. Afin de ne pas introduire un nombre non borné de fragments, nous nous contenterons de distinguer les cellules suivant qu’elles ont été modifiée par l’itération courante de la boucle externe ou par n’importe laquelle des itéra-tions précédentes. Dans notre exemple, ceci distinguerait dans une même itération de la boucle externe les cellules déjà incrémentées de celles qui ne le sont pas encore.

Pour réaliser cela, il va nous falloir ajouter de nouveaux fragments. Nous avons déjà un frag-ment pour le singleton actuellefrag-ment accédé et un fragfrag-ment pour toutes les cellules auparavant accédées par l’instruction. On va décomposer le dernier en deux selon que les cellules ont été accédées lors de l’itération courante de la boucle externe ou lors d’itérations précédentes. Ainsi on aura dans le premier les cellules dont la valeur égale celle deiet dans les seconds celles dont la valeur égale celle dei−1.

On généralise cette idée à tout programme et quel que soit le nombre d’imbrications. On ré-serve une variable pour l’accès courant à une cellule et une variable supplémentaire pour chaque boucle dans laquelle l’accès a lieu. Pour chaque accès et chaque niveau d’imbricationi, on intro-duirait une variableaiqui correspondrait aux accès à des itérations précédentes des jboucles les plus imbriquées mais à l’itération courante de toutes les autres. En fin de compte les fragments qui ne sont pas des singletons sont identifiés par le couple composé d’une référence à un accès et d’une référence à une boucle. Ces fragments servent à collecter les cellules touchées par cet accès durant l’exécution de cette boucle pour les itérations courantes des boucles extérieures.

En terme d’instrumentation, on peut tenter de conserver la même logique de durée de vie introduite plus tôt dans cette section. On conservait les singletons jusqu’à l’accès suivant. On peut conserver les fragments collectant les cellules accédée dans une boucle jusqu’à ce qu’on rentre de nouveau dans cette même boucle. Alors, on donne leurs cellules aux fragments de la boucle englobante avant de les réinitialiser. Appliquons cela à l’exemple précédent. On nommera

a1eta2les fragments singletons pour les deux accès du programme. L’indice désignant l’accès on utilisera l’exposant pour différencier les boucles dont on collecte les accès. Ainsi,a11désigne les cellules accédées par la première affectation dans les itérations précédentes de la première boucle. La boucle interne correspond à l’exposant 2 et la boucle externe à l’exposant 3.

Pour j de1à nfaire a11 ∪:a1 a1 ::{A[j]} A[j] ← 0 Pouri de1à nfaire a32 ∪:a12 a22 :∅ Pour j de1à nfaire a22 ∪:a2 a2 ::{A[i]} A[j] ← A[j]+1

Cette manière de traiter les boucles peut servir à décomposer les tableaux multidimensionnels dans un autre objectif. Dans l’exemple précédent, ce qui nous intéressait était la séparation des effets des différents niveaux de boucles. Dans le cas de tableaux multidimensionnels, cette mé-thode peut aussi servir à simplifier l’abstraction des fragments. Considérons l’initialisation d’un tableau à deux dimensions.

Pouri de1à nfaire

Pour j de1à mfaire

A[i][j] ← 0

L’invariant à l’entrée de la boucle interne se définit aisément de manière formelle. Il peut être désigné de la manière suivante pour un état concretσ.

       A[`1][`2] 1≤ `1 < σ(i) ∧ 1≤ `2 ≤σ(m)∨ `1 =σ(i) ∧ 1≤ `2 < σ(j)       

Le problème de cet ensemble, c’est que la formule le décrivant contient une disjonction. Or, s’il existe dans la littérature des abstractions de fragments acceptant une formule disjonctive, elles viennent souvent avec des inconvénients. En revanche, on trouvera couramment des abstractions adéquates pour les deux conjonctions dont elle est composée.

Si on utilise la méthode précédemment introduite pour décomposer ce fragment, on aura d’une part un fragment contenant les lignes du tableau complètement parcourues et un fragment conte-nant la ligne du tableau en cours de parcours. Ainsi, on peut utiliser cette méthode pour faciliter le travail d’abstraction des fragments.

Dans les deux cas, cette méthode remplit le même rôle : assurer une plus grande régularité des fragments choisis en séparant les effets potentiellement distinct des différentes boucles. Dans le premier cas, on capture la régularité des propriétés de contenu des fragments, dans le second on assure la régularité de la forme des fragments.

Ceci repose néanmoins sur la bonne structuration du programme. On peut transposer la mé-thode à des programmes qui ne sont pas structurés en boucles. Plutôt que d’associer des variables

aux boucles, on peut les associer aux composantes et sous-composantes fortement connexes du graphe de flot de contrôle. Mais il paraît moins évident que la méthode donnera des résultats convaincants sur des graphes de flot de contrôle quelconques.

Ces deux variations peuvent être utilisées pour raffiner les fragmentations. Dans certains cas, ce raffinement améliorera la précision de l’analyse. Dans d’autres, l’effet du raffinement peut être nul et ne faire qu’augmenter la complexité de l’analyse. La première variation conserve un nombre de variables linéaire en la taille du programme aussi longtemps que la durée de vie des variables n’excède pas un nombre borné d’itérations. Dans la seconde en revanche, l’évolution du nombre de variables peut être quadratique puisque ce nombre est proportionnel au nombre d’accès multiplié par le nombre d’imbrications de boucles.

4.1.5 Conclusion

Le critère de fragmentation développé nous donnera quelques succès (Cf. section 10.2). Ce-pendant, on peut s’interroger sur la généralité de celui-ci. De nombreuses variations du critère sont possibles. Si un exemple ne peut être analysé par une variation du critère, il suffit d’en trou-ver une autre qui fonctionnera. Ce problème est récurrent dans le développement d’un interprète abstrait. Qu’il s’agisse des méthodes d’itération ou encore des choix d’opérateurs d’élargisse-ment, on cherche toujours à les améliorer et les généraliser. Le processus est long. L’accumula-tion d’une base de tests toujours plus importante permettra avec un peu de chance de dégager des caractéristiques communes aux échecs du critère de fragmentation et d’en trouver une version plus générale.

Le problème que nous cherchons à résoudre à travers ce critère de fragmentation est semblable à un autre : le partitionnement des variables du programme. Ce problème est capital lorsque l’on analyse des programmes avec plusieurs milliers de variables. On cherchera à constituer des groupes de valeurs abstraites indépendantes. On aura une valeur abstraite pour chaque groupe de valeurs. La complexité des opérations sur les valeurs abstraites étant généralement plus que linéaire, la complexité générale de l’analyse s’en trouve réduite. Or notre critère ne fait pas autre chose que de chercher les relation entre les cellules. Il désigne également par effet de bord les cellules qui n’ont pas de relations et il pourrait être utilisé pour partitionner l’ensemble de variables.

La recherche de relations est néanmoins peu précise. On considère qu’il y a relation lorsque deux cellules apparaissent dans une même instruction ou un même test. D’abord, si le domaine abstrait ne peut exprimer cette relation, alors il n’est pas forcément utile de s’en encombrer. Ensuite, il peut y avoir des relations entre des cellules qui n’apparaissent pas dans la même instruction. Si pour copier un tableau on passe par une variable intermédiaire, on trouvera une relation entre la cellule source et cette variable, puis entre la variable et la cellule destination. Mais notre critère ne trouvera pas la relation entre les cellules sources et destination. Plus sour-noisement, si une constante est affectée à une cellule de tableau et que cette même constante est affectée un peu plus loin à une autre cellule, alors il y a une relation d’égalité entre ces deux cellules, qu’il peut être utile de considérer.

Une piste pourrait être de recourir continuellement à des opérations du domaine abstrait pour savoir si des relations peuvent être découvertes. On pourrait alors améliorer notre fragmentation selon que l’on sait qu’une relation a été trouvée ou bien au contraire qu’on n’a pu en exprimer aucune.

Chercher toutes les relations peut nous mener à un autre écueil. L’existence d’une relation entre deux cellules est généralement transitive de nature. Si une celluleaest en relation avec une cellulebet quebest elle même en relation aveccalors on aura probablement une relation entre

aetc. Pour voir le problème que cela pose, prenons l’exemple d’une propriété exprimant qu’un tableau est trié :

∀`, 1≤` <n⇒A[`]≤ A[`+1]

On aura découvert que les cellules A[`] sont en relation avec les cellules A[`+ 1] et on aura intégré cette relation dans la fragmentation. Mais si A[`] est en relation avecA[`+1] alors on aura par transitivitéA[`] en relation avecA[`+2]. Par transitivité on peut considérer une infinité de relations que nous ne pourrons pas toutes représenter.