Un Environnement Portable
de Programmation
et
de Coopération sur Réseau Hétérogène
pour le Calcul Scientifique,
l'Analyse Visuelle des Résultats
et
les Tests de Sensibilité à la Précision des Calculs
CMAP (Centre de Mathématiques APpliquées) UMR CNRS 7641, École polytechnique, Institut Polytechnique de Paris, CNRS, France
france telecom, France Telecom R&D
[Site Map, Help and Search [Plan du Site, Aide et Recherche]]
[The Y2K Bug [Le bug de l'an 2000]]
[Real Numbers don't exist in Computers and Floating Point Computations aren't safe. [Les Nombres Réels n'existent pas dans les Ordinateurs et les Calculs Flottants ne sont pas sûrs.]]
[N'oubliez pas de visiter Une Machine Virtuelle à Explorer l'Espace-Temps et au-delà où vous trouverez plus de 10.000 images et animations à la frontière de l'Art et de la Science]
(Site WWW CMAP28 : cette page a été créée le 29/05/1995 et mise à jour le 03/10/2024 17:20:30 -CEST-)
(publié dans UNIX'93 Proceedings, Paris, 03/1993)
Introduction: l'étude qui sera ici présentée a été conçue et réalisée par l'auteur à l'Ecole
Polytechnique. Sur le site de cette dernière se trouve implanté un réseau FDDI fédérant de
nombreux réseaux Ethernet, en général locaux à chacun des laboratoires de recherche. Ces voies de
communication rapides supportent le dialogue de plusieurs centaines de machines de tout type et
permettent la pratique intensive du calcul numérique. Ce contexte exceptionnel nous a donc
rapidement amené à nous poser cinq questions :
- 1-Un environnement et une méthode de programmation simples peuvent-ils améliorer la fiabilité
et la maintenabilité des logiciels développés ?
- 2-Cet environnement peut-il être unique et indépendant des systèmes informatiques utilisés (et
donc portable de même que pérenne) ?
- 3-Comment tirer le meilleur parti des ressources disponibles ?
- 4-Comment analyser au mieux les résultats numériques alors produits ?
- 5-Quelle garantie est-il possible d'avoir quant à la validité de ces derniers ?
Ce sont les solutions concrètes, pragmatiques et opérationnelles à quatre de ces cinq
problèmes qui seront données dans les lignes qui suivent. En ce qui concerne le quatrième
problème, sa solution est maintenant bien
connue ; elle ne sera donc que brièvement évoquée par le
biais de quelques images présentées au cours des différents chapitres.
Le plan adopté pour présenter cette réalisation sera le suivant :
1-SECURITE, PORTABILITE ET PERENNITE :
Dans le contexte
du calcul scientifique, pratiquement toutes les machines accessibles
disposent d'un système UNIX ;
c'est donc ce dernier qui a été choisi comme base de travail, sans
faire davantage d'hypothèses restrictives.
Afin d'atteindre l'objectif de portabilité, il convient au préalable
d'étudier et de classer les
différents systèmes en présence, afin d'être à même de définir
une enveloppe qui les contiendra tous
en masquant leurs disparités et ainsi créer un système virtuel idéal.
Ces nombreuses et inévitables
différences sont en particulier (et de façon non exclusive) provoquées par :
- le constructeur,
- la gamme des machines à l'intérieur d'un même constructeur
(par exemple les systèmes
ES9000 et RS6000 sous AIX chez IBM),
- l'origine de l'UNIX supporté (4.2, 4.3, V,...),
- la version du système (release),
- la provenance du ou des compilateurs utilisé(s),
- les défauts (bugs) à tous les niveaux (système
d'exploitation, utilitaires,
compilateurs, bibliothèques,...) que chaque
nouvelle version d'un système corrige,
tout en en introduisant au moins autant de nouveaux...
- et enfin l'action des équipes systèmes administrant les
différentes machines physiques.
Il est à craindre que, malgré les normes, les standards et la volonté
affichée par tous les
constructeurs, des années soient nécessaires pour résorber ces
différences, si tant est que la chose
fut possible (en particulier à cause des bugs dont la plupart sont liés
spécifiquement aux diverses
implémentations...).
1.1-La classification des systèmes :
Pour prendre en compte
ces inévitables différences, deux niveaux de classification
ont donc été distingués :
1.1.1-le niveau logique :
Il définit :
- le constructeur (DEC, IBM,
Silicon Graphics,...),
- le type de machine (RS6000,
VAX9000, 4D20G,...),
- le système d'exploitation (AIX,
Irix, Ultrix,...)
dont la nature est
précisée (4.2, 4.3, V)
ainsi que sa release,
- et enfin le (ou les) compilateur(s)
C disponibles
(cc, gcc, vcc,...).
Ce qui pourrait être considéré comme un système unique,
pourra être défini plusieurs fois s'il
dispose, par exemple, de plusieurs
compilateurs C (c'est
ainsi le cas, au centre
informatique de l'Ecole Polytechnique, du
VAX9000 sous Ultrix sur lequel se trouvent cc,
gcc et vcc simultanément) ; mais, ainsi que cela sera
montré par la suite, ces différentes
"facettes" apparaîtront strictement identiques à l'utilisateur.
Il est à noter que la prise en compte des
différents compilateurs Fortran disponibles est actuellement
à l'étude, ainsi que
cela sera précisé au paragraphe 2.1.8.
1.1.2-le niveau physique :
Il décrit les machines réellement présentes sur les
réseaux,
suivant une notation universelle, hiérarchique
et concise, qui précise :
- d'une part l'identité du laboratoire
ou de l'organisme d'appartenance,
- et d'autre part leur situation a
l'intérieur de ceux-ci.
Bien entendu, ces définitions sont mises en
correspondance avec les adresses IP de ces
machines telles qu'elles sont définies par les
fichiers "/etc/hosts" ou les serveurs de noms (name
servers), mais ce de manière invisible à
l'utilisateur, et tout en tolérant les
inévitables disparités
(au niveau de l'orthographe, ou encore a
celui des synonymes -ou alias-). A titre
d'exemple, le
VAX9000 évoqué ci-dessus s'appelle alors 'INFO23'.
Le nom unique ainsi donné à chaque
machine sera ensuite utilisé lors de toute réfèrenc èa
celle-ci ; c'est ainsi qu'il sera le
mnémonique de
la commande de connexion, mais aussi le "caractère"
d'interrogation (prompt) une fois
la liaison
établie, ou bien encore le nom d'un
serveur (NFS, X11,...) qu'elle
supporte.
Cette distinction entre niveaux logiques et physiques
est nécessaire, car
en effet, d'une part
certaines machines possèdent des ressources uniques
(par exemple une taille mémoire très
importante, ou bien encore un enregistreur
vidéo), et d'autre part les
disponibilités (espace
disque, utilisation du ou des
processeurs,...) varient d'une machine à
l'autre au cours du temps.
Ainsi, les systèmes seront accessibles soit logiquement
(de préférence), soit physiquement,
lorsque cela est rendu nécessaire par certaines contraintes
(au paragraphe 3.4,
nous proposerons une méthode permettant
d'automatiser, dans certaines
circonstances, la sélection
des différentes machines utilisées). Une première
structure apparait donc ici : à son premier niveau des machines
logiques (ou classes), instantiées au second
niveau sous forme de machines physiques (ou instances).
1.2-la machine de référence et les autres :
Dans l'organisation générale
qui est ici présentée, une machine physique joue un rôle
privilégié : elle est dite
machine de référence,
non point, bien entendu, qu'elle soit le centre physique du
dispositif (puisqu'il n'existe pas...),
mais bien plutôt, son centre nerveux. Cette machine (actuellement
une Silicon Graphics INDIGO XS24)
a été choisie physiquement (et non pas logiquement) parce qu'elle est
locale à notre laboratoire, et
qu'ainsi, son administration et sa gestion sont parfaitement
maîtrisables et maîtrisées ; cela concerne
en particulier la politique de sauvegarde et la sécurité (à titre
d'exemple, elle est connectée à un
réseau Ethernet privé, lui-même raccordé au reste du monde
par un répéteur local et désactivable en cas de nécessité).
Son rôle consiste à fournir aux autres machines accessibles, les
dernières versions de
l'ensemble des fichiers utiles (qualifiés par la suite de
fondamentaux) : fichiers de configuration,
sources des programmes, images de base,...
Cette fourniture est réalisée, suivant les cas,
via des opérations implicites ou explicites de copies à distance
par 'rcp' ; le choix d'un volume NFS
commun n'a pas été retenu (uniquement en ce qui concerne ce rôle
de référence) pour des raisons de
sécurité principalement : en particulier la duplication et la
dissémination géographique de
l'information vitale, même si cela peut sembler aller à
l'encontre des principes actuels, minimisent
considérablement les effets d'une panne, d'une catastrophe
ou bien encore d'une malveillance...
Inversement, les autres machines communiquent à la
machine de référence, implicitement ou
explicitement, les fichiers qu'il est nécessaire
d'archiver, et qui ainsi enrichissent et/ou mettent à
jour cet environnement ; il s'agira en général du courrier reçu
sur d'autres systèmes, d'images non
temporaires, de descriptions de séquences d'animations
enregistrées sur un support vidéo et vues
comme des fichiers UNIX, ou encore des fichiers du type
"history" réparti (sans insister sur ce
point, précisons que toutes les commandes interactives
-incluant si nécessaire celles qui sont entrées
sous un éditeur de textes au cours de certaines mises à jour-
sont mémorisées, et qu'elles peuvent
être ensuite retrouvées suivant différents critères -par exemple
chronologiques et "géographiques"-).
Il est essentiel de noter qu'il ne s'agira jamais de fichiers
"fondamentaux", qui eux, ne pourront être
mis a jour que sur la machine de référence
(voir le paragraphe 1.3).
En général, cette machine de référence sert aussi,
par l'intermédiaire de son écran, de point
d'entrée sur les autres systèmes (l'accès se faisant par 'telnet', puis 'ssh' par la suite...).
Toutes les fenêtres 'xterm'
ouvertes, qu'elles soient locales ou bien distantes,
ont des propriétes géométriques (emplacement et
taille) prédéfinies et immuables en fonction de l'usage qui
en sera fait ; ce choix "immobiliste" a été
réalisé afin de permettre à l'utilisateur de se situer très
rapidement par rapport à ses différentes
commandes, et ce à l'intérieur d'un système réparti et
complexe (des fenêtres de toutes les couleurs
et de n'importe quelle taille -jamais la bonne d'ailleurs-,
qui bougent dans tous les sens, si elles
représentent une performance technologique, ne placent pas
nécessairement un certain système a
son optimum d'ergonomie, de sécurité et de rentabiliét
intellectuelle vis a vis de celui qui l'utilise !).
Le dialogue est assuré par la commande 'csh' et le "prompt" choisi
pour chacune d'elles est,
rappelons-le, le nom de la machine tel qu'il a été
défini au paragraphe 1.1.2.
Enfin, ces fenêtres respectent un code de couleur simple
destiné d'une part à faciliter le repérage et la
lecture, et d'autre
part à minimiser les risques de mauvaises manipulations
(par exemple, destruction accidentelle d'un
fichier lorsque l'utilisateur se croit par erreur sur un
certain système) ; avec un écran disposant de la
couleur, le choix suivant a été opéré :
- les fenêtres locales à la machine de référence sont
blanches sur fond noir,
- les fenêtres ouvertes sur des machines distantes
sont jaunes sur fond noir, et enfin,
- les fenêtres qui seraient ouvertes sur la machine
de référence elle-même, via 'telnet'
(cas d'un passage
exceptionnel sous "root" sans utiliser la commande
'su', d'ailleurs inhibée dans ce
contexte), sont rouges sur fond
noir, et ce afin de rappeler les dangers
éventuels encourus.
(en cas d'indisponibilité de la couleur, l'inversion vidéo
serait utilisée dans les deux derniers cas).
1.3-la politique de gestion des fichiers :
Une arborescence extremêment logique et très
hiérarchisée contient l'ensemble des fichiers. Ceux-ci ont un nom
automatiquement limité à 14
caractères (paramêtre ajustable qui correspond à la plus petite
longueur maximale relativement à
l'ensemble des systèmes reconnus), et sont systématiquement
post-fixés ce qui permet de savoir
immédiatement le type du contenu (shell-script exécuté,
shell-script "source", texte, source de
définitions, source de fonctions, fichier de
manœuvre, "exécutable", image...).
Pour des raisons de sécurité, sur la machine de référence
(et éventuellement sur d'autres
machines disposant de suffisamment de ressources),
la sous-arborescence relative aux fichiers
fondamentaux est dupliquée automatiquement sur un disque auxiliaire
(fonction de "miroir synchrone") ; d'autres sous-arborescences peuvent aussi
bénéficier de cette fonctionnalité, auquel cas, elle est en
général différée et effectuée périodiquement (fonction de "miroir asynchrone").
Toujours, en ce qui concerne la sécurité (vis à vis
des erreurs de manipulation principalement),
les fichiers sont tous et sans exception en lecture seule pour leur
propriétaire, et inaccessibles pour
les autres utilisateurs. Leur mise à jour ne peut se faire que via une
procédure spécifique 'maj' (au
fonctionnement interactif ou programmé) qui d'une part "empile" l'état
antérieur (et ce, sur plusieurs
semaines, voire beacoup plus selon certains critères), et
d'autre part édite a posteriori les
modifications apportées sous une forme beaucoup plus lisible, et
plus exploitable dans ce contexte,
que ce que fait habituellement la commande 'diff' ; les trois cas
rencontrés (la destruction, l'ajout et
le remplacement) sont distingués clairement et visuellement.
Cela, associé à la méthodologie qui
sera proposée au chapitre 2, permet d'éliminer de nombreuses
erreurs de codage et de mise a jour,
et participe donc ainsi à l'objectif de fiabilité.
De plus, une "corbeille" a été introduite ; elle est en fait
une hiérarchie dans laquelle la
commande de destruction vient placer les fichiers à détruire ; son
vidage est progressif, et
commence, bien entendu, par les objets à la fois les
plus volumineux et les plus anciens...
Enfin, l'accès aux différents "directories" de l'arborescence se
fait par l'intermédiaire d'alias
(tels que la commande 'csh', qui est le support de
l'interaction, permet de les définir) qui précisent
le chemin relativement au home directory, les créent
si nécessaire, définissent leur masque
spécifique de création des fichiers ('umask') et éditent éventuellement
le contenu du fichier local.nota.t. Ainsi, l'alias '/home/colonna/includes/images/formats' positionne l'utilisateur
dans "$home/includes/images/formats" (cet
exemple indique de plus le procédé utilisé pour générer les noms de ces alias).
1.4-la configuration de l'environnement :
Malheureusement, bien que tous les
systèmes utilisés soient théoriquement assez semblables (puisqu'ils
reposent tous, rappelons-le, sur
une base UNIX), ils contiennent un certain nombre de
différences qu'il convient d'inventorier, de
maîtriser, puis enfin de masquer. Celles-ci sont définies
dans un nombre très restreint de fichiers
(par exemple ceux qui contiennent la définition du
langage K -voir le chapitre 2-, ou encore celui
qui définit les différents compilateurs C et
les options utiles associées). Il est
nécessaire alors de distinguer pour ces fichiers deux types distincts :
- Le type "C-shell-script", où des instructions
'switch' et 'case' permettent de faire, lorsque
cela est nécessaire, la discrimination entre
les différentes possibilités ; ainsi sont définis
conditionnellement -en fonction des niveaux logiques
et/ou physiques définis aux
paragraphes 1.1.1
et 1.1.2- des 'alias' et des
variables, qui seront ensuite utilisés sans
avoir alors à connaitre les
détails spécifiques et les particularités locales.
Il convient de noter que lors d'un login sur une
machine quelconque, une séquence d'optimisation
crée automatiquement, pour chacun des fichiers
les plus utilisés, une version spécifique dans
laquelle ont disparu alors tous les commentaires et
toutes les déclarations non locales (et donc inutiles) ;
ce processus reste caché, et les fichiers créés,
invisibles...
- Le type "module de programme", ou des directives au
pré-processeur cpp ('#if...' par
exemple) assurent une fonction similaire, quel que soit
le langage de programmation utilisé.
Mais insistons encore sur le fait, que ces définitions de
différences entre systèmes logiques et
machines physiques sont extremêment localisées (à l'interieur d'un
nombre extremêment restreint de
fichiers), et que partout ailleurs elles sont complètement
absentes, donnant alors l'illusion de la portabilité absolue...
Notons au passage, qu'un certain nombre d'utilitaires
permettent, en présence d'un nouveau
type de système, d'inventorier très rapidement les
différences, facilitant grandement leur intégration
au cours de l'opération de portage.
Un portage sur un nouveau système demande en général moins d'une
journée (pour plus de
2 000 000 lignes de textes correspondant aux fichiers fondamentaux), et
les éventuels obstacles
rencontrés ne sont en général pas de la nature attendue. Donnons deux exemples :
- la commande 'date' reformatable (c'est-à-dire
permettant, par exemple,
l'écriture "19920430111634" à la
place de "Jeudi 30 Avril 1992 - 11:16:34") est
utilisée dans cet environnement lors de la génération
de noms pour les fichiers temporaires. Une telle
possibilité de reformatage existe sur tous les
systèmes reconnus, à l'exception de
CONCENTRIX (d'origine Alliant).
- les fichiers de commandes destinés à l'éditeur de
textes 'sed' peuvent contenir des
commentaires en nombre quelconque et être placés
n'importe où. Ceci est vrai sauf pour NEWSOS
(d'origine SONY) : une seul ligne est autorisée,
et encore, à condition d'être située en tête du fichier.
Cela constitua les seules vraies difficultés des opérations
correspondantes... Ces petits
exemples montrent que, malheureusement, le
processus de portage ne converge pas, et que chaque
nouvelle tentative peut fournir son lot de difficultés inédites !
Il est essentiel de préciser clairement que "portage" signifie
donc ici "modification d'un nombre
très restreint de fichiers fondamentaux sur la machine
de référence" (suivie des transferts bien
entendu...), et d'insister sur le fait qu'il n'existe
pour chaque fichier qu'une seule version,
commune à l'ensemble des systèmes connus, sachant que,
bien entendu, ils sont ensuite dupliqués là où ils sont nécessaires.
Un tel dispositif fonctionne parfaitement bien, et en
particulier dans les cas difficiles
(notamment lors de références à 'X-Window', où le qualificatif
portable peut être un leurre
dangereux, et la source de difficultés considérables dans un
contexte plus habituel que celui qui est
ici présenté...). Notons au passage que l'un des problèmes rencontrés
vient du fait qu'il n'existe
pratiquement pas de fichiers d'includes systèmes (par
exemple '/usr/include/stdio.h') qui se
retrouvent identiques sur deux machines différentes, en
particulier en ce qui concerne la présence
(ou l'absence) des définitions du type des fonctions de
base (telle 'printf(...)').
Les différences ainsi mémorisées peuvent concerner aussi
bien l'aspect logique que l'aspect
physique de la configuration. Ainsi, par exemple, la
commande 'cp' fera sur la plupart des
machines la copie d'un fichier (associée à diverses
vérifications, sécurité oblige...), alors que sur
certaines autres, elle assurera de plus sa quote-part de la
fonction "miroir" (revoir le paragraphe
1.3). De même sur les ordinateurs de type VAX9000, c'est
le compilateur '/usr/local/bin/gcc'
qui sera utilisé de préférence, alors que sur une station
de travail portable SONY NWS3000, c'est
'/bin/cc2.11', qu'il conviendra de choisir ; mais dans tous
les cas, et quel que soit le système, la
référence d'accès sera la même ('Cc'), cachant de plus les
éventuelles options spécifiques à utiliser
(taille des piles d'analyse, niveau d'optimisation,...).
L'environnement de travail supporte de plus la notion de frontale.
Elle recouvre le fait que
certaines machines (par exemple le nCUBE 2S) ne peuvent être accédées
directement ; leur
utilisation passe obligatoirement par un intermédiaire sur lequel se
font les développements, les
compilations et le lancement des tâches parallèles. Il est ici
possible de supporter des architectures
très générales du type N-frontales vers P-machines, ainsi
que toutes les operations nécessaires.
Enfin, un fichier de configuration joue un rôle particulier : il
contient l'ensemble des problèmes
(bugs) rencontrés sur chacun des systèmes physiques et logiques.
Ainsi une certaine définition
(alias, variable du C-shell, ou encore instruction du
langage K,...) sera donnée de façon
"naturelle" sur les systèmes sans problèmes, et dans une version
corrigée -et provisoire...- sur ceux
où se situent des anomalies. Ensuite, lors de l'apparition d'une
nouvelle version de ces systèmes, si
cette anomalie a été corrigée, il suffira de changer la valeur
d'un indicateur logique contenu dans ce
fichier des bugs, pour que la version "naturelle" rentre en
fonction (la version provisoire, quant à
elle, est conservée bien entendu, afin d'une part
d'accumuler dans ce domaine une certaine
expertise, et d'autre part, de pouvoir la réutiliser
sur un nouveau système ou une anomalie similaire
se présenterait) ; notons d'ailleurs que, lorsque cela est
possible, cette opération est automatisée
par la présence de séquences de tests "cachés"... Quelques exemples
de problèmes rencontrés avec divers compilateurs C
montreront qu'ils sont souvent d'une gravité presque inimaginable :
Ceci n'est malheureusement qu'un petit céhantillon des nombreux
problèmes recensés...
1.5-organisation générale de l'environnement :
Toutes ces notions peuvent être résumées à l'aide
du schéma suivant :
------------------------------------------------------------------------------------------------------------------
| |
| |
| MACHINE de REFERENCE |
| |
| ------------------------------------------------------ |
| | | |
| | FICHIERS ETERNELS | |
| | | |
| | protégés | |
| | une seule et même version ∀ le système cible | |
| | auto-documentés | |
| | | |
| | ---------------------------------- | |
| ------------------- | | | | |
| | | | | fichiers de configuration | | |
| | miroir synchrone |<====|========>| sources des programmes | | |
| | | | | textes (articles, courriers...) | | |
| ------------------- | | | | |
| | ---------------------------------- | |
| | | |
| | ---------------------------------- | ------------------- |
| | | | | | | |
| | | images fixes et animées |<========|====>| miroir asynchrone | |
| | | | | | | |
| | ---------------------------------- | ------------------- |
| | | |
| ------------------------------------------------------ |
| /\ || |
| /||\ || |
| || || |
| || || |
------------------------------------------------------------------------------------------------------------------
|| || || /\
|| || || /||\
|| || || ||
|| RCP d'enrichissement || || RCP de mise à jour ||
|| de l'environnement || || des autres machines ||
|| (historiques, images...) || || et systèmes utilisés ||
|| || || ||
\||/ || \||/ \||/
\/ || \/ \/
---------------------------- ---------------------------------- ----------------------------
| | | | | |
| | | | | |
| SAUVEGARDES ("BACKUPS") | | | | VOLUMES NFS |
| | | | | |
| | | AUTRES MACHINES UTILISEES |<========>| |
| quotidiennes | | | | fichiers temporaires, |
| et non incrémentales | | | | outils de synchronisation |
| | | | | |
| | | | | |
---------------------------- ---------------------------------- ----------------------------
ou la notion d'éternité signifie "a priori non destructible"...
2-LE LANGAGE K ET SON MODELE DE PROGRAMMATION :
Il est parfois nécessaire de récupérer des programmes écrits par d'autres (exemple vécu : celui du
source du pré-processeur cpp, auquel nous nous
référerons à plusieurs reprises par la suite...),
ou bien par soi-même quelques années auparavant. La liste des difficultés rencontrées alors est
longue :
- absence de commentaires,
- utilisation de constantes "ésotériques",
- expressions complexes dont la signification est obscure,
- structures profondément imbriquées les unes dans les autres,
- hypothèses implicites,
- etc...
autant d'obstacles qui transforment la mise à jour et la correction d'erreurs en exercices
souvent pénibles et toujours périlleux. En l'absence généralisée d'outils du type CASE (Computer
Aided Software Engineering) utilisés systématiquement et disponibles sur tous les systèmes (en
particulier dans le domaine scientifique), une méthode logique, accompagnée
d'une discipline de fer
et sans exception, peuvent aider grandement, d'une part à réaliser
plus rapidement des logiciels
d'une plus grande fiabilité, et d'autre part à faciliter leurs évolutions
ultérieures, tout en
faisant des
documents de travail et de référence utiles et communicables.
2.1-Quelques remarques préliminaires :
(toutes les remarques qui suivent sont
indépendantes du langage réellement utilisé -voir par exemple le source
d'un programme C de génération
d'autostéréogrammes
ou encore celui de la résolution d'un problème d'arithmétique- ;
sans aller juqu'a la programmation en K -telle qu'elle sera
définie au paragraphe 2.2-, l'expérience
montre que la pratique du Fortran, par exemple, peut
grandement bénéficier du pré-processeur cpp du C)
2.1.1-Des commentaires utiles :
Ceux-ci doivent accompagner généreusement le code, et
être rédigés simultanément, voire au préalable, et
non point a posteriori, lorsque le fil du raisonnement se
sera émoussé.
Par la suite, lorsque ce programme subira des mises à jour,
les commentaires correspondant devront, eux-aussi, être modifiés.
Pour améliorer la présentation et la lisibilité du
programme, il est de loin préférable de rédiger
des lignes de commentaires distinctes des instructions
elles-mêmes, dont la longueur et la tabulation
soient constantes et uniformes à l'intérieur d'un ensemble
de logiciels (remarque tout aussi valable
pour les instructions d'ailleurs...), tout en distinguant clairement
ceux qui constituent des séparateurs à l'intérieur d'un même fichier,
des commentaires d'instructions
(par exemple, en les faisant débuter en première
et quarantième colonne respectivement).
Toute ligne commentaire devra être matérialisée en tant que tel,
c'est-à-dire commencer et se terminer par des indicateurs de début et
de fin ("/*" et "*/" pour un programme en C) même si elle
fait partie d'un ensemble plus volumineux.
Bien entendu cette contrainte
peut être vue comme une perte de temps, mais il convient
de ne pas oublier que cela peut être réalisé
facilement par des duplications ou bien encore des opérations du
type "copier-coller" ("copy and paste").
De plus, lors du commentaire de formules
complexes, il est en général utile de les faire
apparaître sous leur forme naturelle (mathématique), avec
par exemple des traits de fraction, des
"sigmas",... Encore une fois tout cela peut paraître
pénible, mais avec un peu d'habitude et
d'entraînement, la chose est tout à fait possible.
D'autre part, il n'est pas inutile parfois d'indiquer
clairement les modifications apportées par
rapport à une version antérieure lors de la correction d'un
défaut (qu'il soit interne ou externe -par
exemple "trappe" d'un compilateur-), ou bien
encore les raisons de tel ou tel autre choix. Il est
alors important d'indiquer la date à laquelle a été
réalisée cette opération. A ce propos, il est
suggeré de plus de mettre la date dans le
format universel 'AAAAMMJJhhmmss'
pour lequel l'ordre alphabétique est
aussi l'ordre numérique et qui facilite ensuite d'éventuelles
recherches automatiques ; Il convient de noter au passage que la
commande UNIX :
date "+%Y%m%d%H%M%S"
réalise automatiquement cette mise en forme sur la plupart
des systèmes accessibles.
Il est de plus suggéré de mettre en place des liens inter-fichiers
permettant de préciser les relations qu'ils entretiennent entre-eux.
Par exemple, si un certain algorithme a été mis au point
à l'aide d'un programme P1 pour être utilisé ensuite dans un
programme P2 plus complexe, il sera utile de l'indiquer
dans P1 et dans P2. Cela est aussi vrai pour des fichiers de
données, de résultats ou encore des images visualisant
ceux-ci.
Le résultat obtenu est alors un véritable outil de travail
auto-suffisant (dans le sens où aucun
document accessoire n'est plus nécessaire...).
2.1.2-De la paramétrisation :
Imaginons qu'à l'examen d'un programme, nous
trouvions,
par exemple, dans deux instructions la constante 511 ;
la question qui se pose alors est de savoir si
les deux occurences du nombre 511 sont attachées au même objet ? Et
cette (ou ces) constante(s)
511 a-t-elle (ont-elles) un rapport avec la constante
512 rencontrée quelques lignes auparavant...
Cela correspond en fait à la triste réalité : nombreux
sont en effet les programmes où abondent ainsi
des valeurs numériques littéralement parachutées, et où
toutes les
relations qu'elles peuvent entretenir ont complètement disparues.
En fait, quoi de plus facile que de définir des constantes
fondamentales (des "atomes") en les
plaçant, par exemple, dans un fichier
unique, puis en les référencant symboliquement et sans
exception (le source du programme cpp
contient ainsi une définition dont l'usage n'est pas
systématique...). Ensuite, à partir d'elles,
des "molécules" de plus en plus complexes seront
assemblées ; elles permettront de comprendre alors,
en reprenant l'exemple ci-dessus, que l'une des
occurences de la constante 511 désignait en fait l'ordonnée
maximale de l'axe des ordonnées, alors que l'autre représentait
un masque de 9 bits (511 étant égal a 2 puissance 9 moins 1)...
Il pourrait paraitre banal de dire qu'une telle pratique :
- respecte la logique et la structure des
objets manipulés,
- simplifie la rédaction d'un programme,
- améliore sa lisibilité
- facilite ensuite grandement sa
mise à jour (en particulier avec des méthodes automatiques),
mais une fois encore, l'expérience montre que
peu nombreux sont ceux qui profitent de ses
bienfaits, quel que soit le langage de
programmation utilisé... Ce
qui pourrait être qualifié ici de naïveté n'est en
fait que du bon sens...
Cette approche "chimique" de la paramétrisation peut et doit aussi se retrouver
dans la définition des structures, des opérateurs, des fonctions,... utiles.
2.1.3-Contre le laxisme et prévoir l'avenir :
Pour alléger la tâche du
programmeur, de nombreux langages
autorisent, dès que la chose est
possible, certaines
simplifications et ne font pas de la syntaxe, un
carcan suffisamment rigide. Si cela peut effectivement
faire gagner quelques précieuses secondes
lors de la création d'un nouveau programme,
ces facilités
peuvent a posteriori se révéler nuisible,
voire dangereuse. Par exemple,
en C, il
est possible d'écrire :
if ( < condition > ) < instruction non composée 1 >
(le point-virgule étant ici inclu dans la définition
d'une instruction non composée) ; puis,
supposons que, lors d'une évolution du programme
ou à l'occasion d'un test, une deuxième
instruction (par exemple l'édition d'un message),
soumise à la même condition soit à rajouter à la
suite de la première ; cela ne s'écrira pas (même si
la forme suivante est syntaxiquement correcte et
d'une signification toute différente...) :
if ( < condition > ) < instruction non composée 1 > < instruction non composée 2 >
mais :
if ( < condition > ) { < instruction non composée 1 > < instruction non composée 2 > }
dans ces conditions, pourquoi ne pas contraindre
à écrire dans tous les cas :
if ( < condition > ) { < instruction > }
et donc en particulier :
if ( < condition > ) { < instruction non composée 1 > }
Ainsi que le montre clairement ce petit exemple, une telle
contrainte facilite la mise à jour
ultérieure et permet même certaines modifications automatiques (par
exemple à l'aide de fichiers de
commandes destinés aux programmes d'édition et de manipulation de
textes -'sed', 'awk',...-). Il
est possible d'aller encore plus loin sur cette voie ; en reprenant
l'exemple du test, la présence d'une
séquence alternative (éventuellement vide) pourrait être rendue
obligatoire, soit :
if ( < condition > ) { < instruction 1 > } else { < instruction 2 > }
Bien évidemment, cette suggestion s'étend à de nombreuses
autres structures du C (par exemple aux
boucles 'for') et bien entendu, aux autres langages de programmation.
La compréhension de la structure des programmes se
trouve ainsi grandement ameliorée, en
particulier lorsque de nombreux tests se trouvent imbriqués
les uns dans les autres et/ou avec
d'autres constructions.
A titre d'exemple, le source du pré-processeur
cpp a subi récemment une cure de jouvence
en mettant en pratique ces quelques principes ; l'appréhension
de ses complexes mécanismes
internes s'est vue alors effectivement facilitée,
même si les goto qu'ils contient n'ont pu être
supprimés... Le langage K, que
nous proposons, respecte bien évidemment ces principes...
2.1.4-Des symboles évocateurs :
Toujours dans l'idée de faciliter la compréhension d'un
programme, il est évident que le rôle joué par
les symboles utilisés pour désigner les objets
manipulés, est important. Pourquoi abuser de noms
du type 'X123' ou 'NDG12A', alors que
'position_de_la_source_lumineuse' ou 'parametre_d_interpolation'
sont beaucoup plus évocateurs,
correspondent à la logique du problème traité
(notons au passage que c'est en particulier l'absence d'une terminologie
homogène qui rend impossible l'automatisation du traitement du
problème de l'An 2000)
et enfin demandent un effort de mémorisation beaucoup moins
important, voire nul ?
Il pourrait être
répondu à cela, qu'il est plus beaucoup plus
facile de taper sur un clavier 'X123' que
'position_de_la_source_lumineuse' ; en fait cet argument n'est
d'aucune valeur, car en effet,
de même qu'au paragraphe 2.1.1,
les opérations "copier-coller" ("copy and paste"),
éventuellement associées à une fenêtre spéciale de type
"scratch pad", peuvent et
doivent être utilisées. De plus, ce
type de symbole évocateur limite les risques d'erreur
liés a l'usage de
symboles "voisins" l'un de l'autre (par exemple 'X123' et 'X213').
Enfin, lors de l'utilisation de langages tel le
Fortran, ou les variables scalaires, par
exemple, peuvent être définies implicitement,
il convient de renoncer à cette facilité, et de les
déclarer explicitement afin d'éviter de voir le compilateur
créer plusieurs variables différentes, par exemple
à la suite de fautes de frappe du programmeur...
2.1.5-De la difficulté de
comprendre une expression :
La syntaxe d'un langage de
programmation peut être qualifiée de linéaire,
alors que celle qui est utilisée en mathématiques ne
l'est en général pas (notons dès à present,
que l'un de nos
objectifs, est ici de rétablir la "non
linéarité" au niveau de la programmation).
Par exemple, les deux formules arbitraires suivantes :
f5(x)-a
y1 = f1(f2(f3(x)*f4(x)) + --------- + f7(f8(x)*f9(x)))
f6(x)-b
f2(f3(x)*f4(x)) + (f5(x)-a)
y2 = f1(-----------------------------)
(f6(x)-b) + f7(f8(x)*f9(x))
s'écrivent (dans un certain langage de programmation) :
y1 = f1(f2(f3(x)*f4(x)) + (f5(x)-a)/(f6(x)-b) + f7(f8(x)*f9(x)))
y2 = f1((f2(f3(x)*f4(x)) + (f5(x)-a))/((f6(x)-b) + f7(f8(x)*f9(x))))
Cette dernière écriture :
- est relativement illisible,
- ne facilite pas la compréhension des
deux formules, les
grandes structures présentes (et en
particulier les deux formes principales
f1(A+B+C)
et f1(P/Q)) n'étant pas mises en évidence,
- est ambigüe (puisque les deux formules paraissent à
première vue pratiquement identiques...).
Or malheureusement, il est possible d'exhiber facilement des
exemples d'une complexité très
supérieure, en particulier dans l'univers de la programmation
scientifique. La linéarité du source fait
perdre la structure arborescente de la formule et en limite par là-même
la compréhension (tout en rendant difficile sa mise à jour...).
Cette structure peut être restituée visuellement à condition
d'utiliser une notation pré-fixée
associée à des retours à la ligne fréquents ainsi qu'à
une tabulation systématique et logique. Ainsi
sera clairement exhibée la structure de la formule
programmée, cette propriété se conservant
quelqu'en soit la complexité. Mais de plus,
cette écriture facilitera
les mises à jour, par exemple, par
le remplacement d'une sous-expression par une autre.
D'autre part, elle fait jouer un role identique
aux opérateurs arithmétiques ('+', '*',...)
et aux fonctions
('cos', 'sin',...). Ceci nous invite à
proposer une syntaxe où seules seraient définies des fonctions.
Les deux formules ci-dessus
pourront alors s'écrire (en notant bien l'importance de
la tabulation) :
EGAL(y1
,f1(ADD3(f2(MUL2(f3(x),f4(x)))
,DIVI(SOUS(f5(x),a)
,SOUS(f6(x),b)
)
,f7(MUL2(f8(x),f9(x)))
)
)
);
et :
EGAL(y2
,f1(DIVI(ADD2(f2(MUL2(f3(x),f4(x)))
,SOUS(f5(x),a)
)
,ADD2(SOUS(f6(x),b)
,f7(MUL2(f8(x),f9(x)))
)
)
)
);
(d'autres formes, plus ou moins condensées,
sont bien évidemment possibles).
Un nouvel avantage de cette notation apparait alors ;
imaginons que les deux additions
('ADD2') de la deuxième expression soient à remplacer par
une recherche de maximum (fonction
'MAX2') : il suffit alors de substituer 'MAX2' à 'ADD2'.
L'expression garde alors, ce
qui est tout a fait remarquable, la même forme.
Il faut remarquer qu'un tel principe peut être respecté
lors de l'utilisation de langages plus conventionnels
(le C, le Fortran,...),
même si cela est moins facile à réaliser. Ainsi,
nos deux expressions précédentes pourraient alors s'écrire :
y1 = f1(f2(f3(x)
*f4(x)
)
+((f5(x)-a)
/(f6(x)-b)
)
+f7(f8(x)
*f9(x)
)
)
y2 = f1((f2(f3(x)
*f4(x)
)
+(f5(x)-a)
)
/((f6(x)-b)
+f7(f8(x)
*f9(x)
)
)
)
ou bien encore, dans une version "allégée" :
y1 = f1(f2(f3(x)*f4(x))
+((f5(x)-a)/(f6(x)-b))
+f7(f8(x)*f9(x))
)
y2 = f1((f2(f3(x)*f4(x))+(f5(x)-a))
/((f6(x)-b)+f7(f8(x)*f9(x)))
)
Dans les deux écritures précédentes, il convient
de bien noter l'utilisation de parenthèses redondantes,
utiles à la structuration, à la compréhension et à la
modification des formules...
Il s'agit alors d'une programmation qu'il serait possible de
qualifier de visuelle.
En effet,
les symétries et les régularités intrinsèques des expressions
peuvent apparaître à l'écran sous la
forme de motifs à deux dimensions, et ce d'autant
plus que les noms des primitives seront
composés du même nombre de caractères ; dans le
langage K, c'est le cas des plus élémentaires
d'entre-elles : un code mnémonique à quatre caractères à été
choisi (il est alors associé à une
tabulation de cinq en cinq -quatre caractères de
dénomination, plus un pour la parenthèse
ouvrante-). Notons au passage que cette même tabulation
se retrouve, en particulier, au niveau de
l'imbrication des structures du langage
(tests, itérations,...).
L'expérience montre qu'ainsi de
nombreuses erreurs de codage ou de copies (à partir d'un
formulaire de mathématiques par
exemple...) peuvent être éliminées très simplement,
à l'aide d'un simple coup d'œil, à la recherche
d'irrégularités, d'absence de symétrie,...
en regardant le
programme -telle une image-, sans le lire
-tel un texte-. Des utilitaires sont disponibles afin
de vérifier le bon respect de cette discipline...
Ainsi, simultanément la lisibilité et la "malléabilité"
des expressions s'améliorent grandement,
permettant un pas de plus vers la fiabilité des logiciels.
Un certain nombre de fonctions "atomiques"
sont ainsi définies dans le langage K ;
elles correspondent :
- aux opérations arithmétiques et logiques
élémentaires,
- à la définition des objets et à leur accès
(par exemple, l'indexation
d'une matrice se fera à
l'aide d'une fonction spécifique -éventuellement
conditionnée par
l'environnement tel que cela fut
défini au
paragraphe 1.4-
permettant ainsi
de faire abstraction des problèmes liés à l'ordre de
rangement des éléments d'un tableau dans
la mémoire et aux
performances des programmes
correspondants, ou de passer
simplement de dimensions
constantes à des dimensions
variables, ou
bien encore de répartir sa gestion sur
plusieurs processeurs),
- aux structures de programmation
(tests, itérations,...),
- etc...
Puis, dans une démarche similaire à celle qui
fut décrite dans le
paragraphe 2.1.2 traitant de la
paramétrisation, des fonctions "moléculaires" de plus en plus
complexes sont assemblées, et placées
soit dans un fichier fondamental et commun à l'ensemble des
logiciels (c'est
la définition du langage
K...), soit dans des fichiers spécifiques (dits
d'extension de K) liés à des applications particulières
(algèbre linéaire, traitement d'images, synthèse
d'images, physique des particules,...). Ces
fonctions sont définies à partir de l'examen, au cours
du temps, d'un
grand nombre de problèmes
dans lesquels sont recherchées des formes syntaxiques
communes (par exemple, des formes
linéaires du type 'a*x+b' -les trois
références 'a', 'x'
et 'b' étant arbitraires-, les produits
scalaires, la
"règle de trois",...). Nous ne faisons pas ici
d'hypothèses quant
à la définition de ces fonctions ;
elles peuvent par exemple prendre en compte conditionnellement
(suivant le type logique -et
éventuellement physique- du système hôte) certaines spécificités
des compilateurs, ou
malheureusement les éventuelles anomalies -ou bugs-
constatées (revoir le paragraphe 1.4).
2.1.6-Prévoir les anomalies :
Dans l'état actuel, aucune méthode ne permet d'éliminer
de façon certaine toutes les erreurs de conception.
Malheureusement, celles-ci peuvent
échapper aux tests préliminaires pour n'apparaître que
beaucoup plus tard. Une aide alors utile, peut être
d'insérer, dans les programmes, des tests de
validité de certaines des valeurs calculées.
Par exemple, si une grandeur ne peut qu'être
strictement positive, un test vérifiant cette
condition pourra être ajouté après son calcul ; en cas de
violation, les différentes valeurs utiles à son
obtention devront être éditées afin de faciliter la
correction de l'anomalie.
Celà signifie de plus que la notion d'hypothèse implicite
doit être à proscrire absolument. Ainsi, par
exemple, même si un paramètre d'appel d'un
programme ou d'une fonction n'a de sens que positif
(un "nombre de"), il est essentiel
de vérifier explicitement qu'il en est bien ainsi ; dans le
cas contraire, il conviendra d'éditer des messages
significatifs puis, suivant les circonstances,
d'interrompre l'exécution ou de forcer une valeur par défaut
(ceci devant être indiqué clairement...). A titre d'exemple,
un grand constructeur distribue un simulateur de machines parallèles
sur machines séquentielles UNIX. Ce logiciel manipule systématiquement
les numéros de processeurs virtuels (ainsi que les paramètres
associés) à l'aide de décalages, faisant ainsi l'hypothèse
implicite que le nombre de processeurs demandés par l'utilisateur
est une puissance de 2. Malheureusement, cela n'est pas dit
et n'est jamais testé. Il est donc possible, par exemple,
de demander 5 processeurs virtuels ; la simulation se bloque alors
rapidement, sans explications...
2.1.7-Rechercher la simplicité :
Le temps n'est plus où la programmation de
machines à la mémoire centrale de quelques kilo-octets se faisait en assembleur.
Les compilateurs d'aujourd'hui ont atteint, en particulier dans
le domaine de l'optimisation, un niveau de compétence que peu
d'entre-nous possèdent.
Par exemple, sur une O200 (R10000) Silicon Graphics
en version 6.4 du système IRIX et 7.1 des compilateurs,
le programme suivant (calculant un produit matriciel) :
#include < stdio.h >
#define N 500
int main()
{
double MatriceProduit[N][N],Matrice1[N][N],Matrice2[N][N];
double trace;
int i,j,k;
for (i=0 ; i < N ; i++)
{
for (j=0 ; j < N ; j++)
{
Matrice1[i][j] = i+j;
Matrice2[i][j] = i*j;
/* Initialisation arbitraire des deux matrices. */
}
}
for (i=0 ; i < N ; i++)
{
for (j=0 ; j < N ; j++)
{
MatriceProduit[i][j] = 0;
for (k=0 ; k < N ; k++)
{
MatriceProduit[i][j] = MatriceProduit[i][j]
+ ( Matrice1[i][k]
*Matrice2[k][j]
);
}
}
}
trace = 0;
for (i=0 ; i < N ; i++)
{
trace = trace + MatriceProduit[i][i];
}
printf("\n trace=%f\n",trace);
/* Le calcul de la trace, ainsi que le 'printf(...)' sont uniquement destines a utiliser */
/* les resultats calcules. En effet, sinon l'optimiseur reduit ce programme a neant */
/* puisqu'alors il ne sert effectivement a rien... */
}
(où sont mises en gras les lignes utiles au calcul du produit ; il est
à noter la présence d'un code calculant
et surtout éditant la trace de la matrice : ces lignes sont rendues nécessaires par
l'utilisation de l'optimiseur qui est capable de repérer un programme
qui ne sert à rien et qui dans ces conditions élimine alors l'intégralité des
instructions...)
s'exécute en un temps T (=28.5 secondes) lorsqu'il est compilé suivant :
cc -64
et en T/25 avec :
cc -64 -O3
Evidemment et malheureusement, ce rapport sera en général
moins élevé pour des programmes plus complexes et moins "académiques" ; malgré cela,
même dans des cas "très séquentiels", où les optimisations à
réaliser sont peu évidentes, des gains parfois surprenants
pourront être obtenus.
Dans ces conditions, pourquoi réduire fortement la lisibilité
du programme en introduisant d'obscures optimisations, alors que celles-ci
pourront être effectuées automatiquement et mieux par les
compilateurs (ces optimisations pouvant être, de plus,
non portables -en effet, bien souvent elles sont liées à l'architecture
matérielle des mémoires et des processeurs utilisés-...) ?
Cette remarque s'applique aussi bien aux
instructions de calcul arithmétique qu'aux tests logiques ; à
titre d'exemple, il sera souvent plus lisible de remplacer un
test complexe par une série de tests plus simples et imbriqués
les uns dans les autres.
2.1.8-Introduction au langage
K :
Afin d'introduire les quelques règles de bon sens
précédentes, nous avons donc constitué une bibliothèque
contenant les définitions de ces fonctions
(donnant ainsi naissance au langage K) et se
situant au-dessus des langages existants (le C,
le Fortran,...) ; le modèle de programmation ici présenté
est illustré à l'aide
du programme K
d'étude de l'atome d'Hydrogène et qui sera décrit au
paragraphe 2.3. Deux points
importants sont alors à noter :
d'une part leurs arguments ne sont pas typés, ce qui
permet une réutilisabilité importante (bien que
non universelle), et d'autre part, une fois qu'un
programme n'utilise plus que ces définitions, il est
devenu indépendant du langage de programmation sous-jacent
(et référencé dans les définitions),
puisque K absorbe et masque les différences
(et les bugs en particulier : revoir à ce propos
le paragraphe 1.4).
Un programme écrit en K sera d'abord
traduit en un autre
langage (le C actuellement) avant
d'être compilé puis exécuté. Cette opération de traduction se
faisant principalement à l'aide de règles
de réécritures, K permet la
manipulation symbolique du texte
source ; c'est ainsi qu'il contient,
entre autres choses, un dérivateur formel (partiel et
à l'ordre N) défini par l'ensemble minimal
des règles classiques de la dérivation, et par exemple :
d_MUL2(u,v) ==> ADD2(MUL2(d#u,v),MUL2(u,d#v))
d_F_de_G(derivee_de_F,G) ==> MUL2(derivee_de_F,d#G)
d_LOGX(x) ==> d_F_de_G(INVE(x),x)
qui définissent respectivement les dérivées :
- d'un produit de deux facteurs quelconques,
- d'une fonction de fonction,
- du logarithme,
'ADD2(,)', 'INVE()' et 'MUL2(,)'
désignant respectivement la somme de deux termes,
l'inverse d'une expression et le produit de deux facteurs.
Le langage K et son environnement
complet de travail sont actuellement portés sur les
systèmes suivants :
- Alliant FX2800 sous CONCENTRIX-fxc/pcc/scc,
- Alliant FX40 sous CONCENTRIX-cc/fxc,
- BULL DPX2000 sous SPIX-cc,
- BULL DPX5000 sous SPIX-cc,
- BULL SPS9 sous ROS-cc/rc,
- CRAY2 sous UNICOS-cc/scc,
- CRAY Y-MP sous UNICOS-cc/scc,
- CRAY C90 sous UNICOS-cc,
- DEC VAX sous ULTRIX-cc/gcc/vcc,
- DEC VAX-station sous OSF-cc,
- Hewlett-Packard HP700 sous HPUX-cc,
- IBM ES9000 sous AIX-cc,
- IBM RS6000 sous AIX-cc,
- Silicon Graphics 4D sous IRIX-cc,
- Silicon Graphics INDIGO sous IRIX-cc,
- Silicon Graphics O200 sous IRIX-cc,
- Silicon Graphics Power-Challenge sous IRIX-cc,
- SONY NWS3000 sous NEWSOS-cc/cc2,
- SUN sous SUNOS-cc,
- SUN/nCUBE2S sous SUNOS-ncc,
- Tout système sous LINUX-gcc.
Il convient de noter que des primitives d'un autre langage que
le C pourraient être utilisées : le
Pascal très facilement, mais surtout le Fortran dont
les compilateurs sur les calculateurs vectoriels
sont en général beaucoup plus performants que ceux du
C (tant en ce qui concerne la durée de la
compilation que la qualité du code binaire généré). En ce qui
concerne le Fortran, les problèmes
rencontrés alors ne se situent pas uniquement, ainsi que
l'on pourrait le croire, au niveau des
structures, de la récursivité, des
pointeurs, ou bien encore de l'allocation dynamique de
mémoire,
mais malheureusement, aussi, aux niveaux
d'archaïsmes souvent encore présents (symbôles de
taille très limitée, indicateurs de lignes suite,
troncation à partir de la soixante-treizième colonne,
longueur limitée des expressions,...). Il convient de
noter que d'une part toute définition de
structure (au sens large du terme), ainsi que l'accès
à l'un quelconque de ses éléments, passant par
des fonctions et que d'autre part les pointeurs n'étant pas
explicitement définis, la traduction
Fortran sera grandement simplifiée. Cette traduction est
actuellement à l'étude...
De plus, le langage K facilite
le prototypage et est extensible. Le terme prototypage doit être
entendu ici avec deux significations différentes : d'une
part K permet l'utilisation d'"objets"
incomplètement définis, et d'autre part, il
est possible de créer des générateurs (ou "squelettes") de
fonctions, de structures,... qui pourront être
ensuite utilisés pour obtenir des instances spécifiques,
fonction de paramêtres particuliers (qui peuvent être,
par exemple, des séquences d'instructions...).
A titre d'illustration de ce dernier point, indiquons
qu'a été
programmé en K un générateur récursif
des différents opérateurs nécessaires aux calculs dans les
corps des Complexes C, des Quaternions
Q, des Octonions O,... Si leurs définitions
sont élementaires dans C, elles sont par contre beaucoup
plus difficiles à formuler explicitement dans Q et a fortiori
dans O, ou, par exemple, la
multiplication n'est ni commutative, ni associative...
Le générateur proposé définit les opérations
dans un corps quelconque à l'aide de celles du corps
sous-jacent (conjugaison, addition,
multiplication et calcul du module principalement). Soit K2
un corps dont les éléments (r, a, a1,
a2,...) sont définis comme des doublets d'éléments du
corps K1 (par exemple K1=R et K2=C, ou
encore K1=C et K2=Q) et notés :
r = {R(r),I(r)} ∈ K2
R(r) ∈ K1
I(r) ∈ K1
R(r) et I(r) représentant respectivement, par abus de
langage, les parties Réelle et Imaginaire du
nombre r. Les opérations fondamentales dans K2 seront définies
à partir de celles de K1 par les règles suivantes :
r = a ==> {R(r),I(r)} = {R(a),I(a)}
r = -a ==> {R(r),I(r)} = {-R(a),-I(a)}
_ ____
r = a ==> {R(r),I(r)} = {R(a),-I(a)}
r = a1+a2 ==> {R(r),I(r)} = {R(a1)+R(a2),I(a1)+I(a2)}
_____ _____
r = a1*a2 ==> {R(r),I(r)} = {(R(a1)*R(a2))-(I(a1)*I(a2)),(R(a1)*I(a2))+(I(a1)*R(a2))}
2 2 2 2
|a| ==> |a| = |R(a)| +|I(a)|
sachant qu'un nombre réel est égal à son conjugue :
_
x = x ∀ x ∈ R
Référencé ensuite avec les arguments appropriés (en particulier
le nom des corps K1 et K2
utilisés), il produira automatiquement les opérations
typées dans le corps correspondant. Calculer
ensuite des ensembles fractals dans C ou dans Q se fera alors
strictement à l'aide des mêmes
programmes... K facilite donc
ainsi la réutilisabilité.
En ce qui concerne l'extensibilité, il est aisé de
créer, en vue d'une application particulière, un
jeu de "primitives" (éventuellement incomplètes,
voire vides...) supplémentaires et adaptées à
celle-ci. Plusieurs bibliothèques d'extensions d'intérêt
général sont déjà disponibles
(mathématiques, synthèse d'images, traitement
d'images, programmation objet...) ; mais elles
peuvent être beaucoup plus spécifiques : c'est ainsi que
pour la réalisation d'un film sur la structure
des nucléons (dans le cadre du modèle en quarks et gluons de
l'interaction forte), ont été ajoutées
des instructions traduisant les diagrammes de Feynman ; elles
permirent alors de programmer très
aisément et de façon naturelle, les différents
mécanismes élémentaires (voir le chapitre 5).
Enfin, bien que l'utilité n'en soit point évidente
dans ce contexte, le langage K étant
préfixé, il
se prête parfaitement à la programmation de méthodes dites
"génétiques" (tout comme le LISP),
très utilisées, par exemple, pour la
génération de textures. Rappelons que celles-ci reposent d'une
part sur la "mutation" (c'est-à-dire la modification arbitraire
et en général aléatoire d'un opérateur
et/ou d'un opérande), et d'autre par sur le "croisement"
(correspondant à l'échange d'opérateurs
et/ou d'opérandes entre deux expressions lors du processus
appelé "reproduction"). En effet,
l'écriture préfixée permet de manipuler facilement,
et à toutes les échelles, la structure d'une
expression.
Cela a aussi permis d'introduire très facilement
un dispositif permettant d'imposer, aussi bien localement et très finement
que globalement, l'ordre dans lequel les différentes instructions
élémentaires d'un programme seront exécutées.
En effet, puisqu'en toute généralité :
F.G # G.F
('F' et 'G' désignant deux fonctions arbitraires), les compilateurs
n'ont donc absolument pas le droit de remplacer 'F(G(...))' par 'G(F(...))'.
Lorsque ce dispositif est actif, les quatre opérations arithmétiques
élémentaires sont effectuées via les quatre fonctions :
fADD2(x,y) = ADD2(x,y)
fSOUS(x,y) = SOUS(x,y)
fMUL2(x,y) = MUL2(x,y)
fDIVI(x,y) = DIVI(x,y)
(où les opérateurs {ADD2,SOUS,MUL2,DIVI} sont définis de façon naturelle au paragraphe 2.2.3).
En temps normal, ce dispositif est désactivé, mais, comme
cela est décrit par la suite, dans de
nombreuses circonstances le résultat d'un calcul peut dépendre, de façon plus
ou moins importante, de cet ordre. Ainsi, par exemple,
ces deux images et ,
calculées à l'aide d'un processus non itératif
sur la même machine (un PC sous Linux), avec et sans imposer l'ordre respectivement,
semblent identiques alors qu'elles sont en réalité
légèrement différentes. Un tel dispositif
permet donc de garantir l'obtention des mêmes résultats sur des machines
différentes (ou bien encore sur la même machine, mais après des évolutions
concernant en particulier les compilateurs...)
en imposant l'ordre d'exécution des opérations, celui-ci
étant choisi en accord avec la nature du problème traité...
Enfin, dernier avantage de l'écriture préfixée : elle permet
de redéfinir dynamiquement l'arithmétique utilisée. Il suffit d'étendre
la définition des quatre fonctions précédentes (l'opérateur 'ADDn' étant celui de l'addition à n termes) :
fADD2(x,y) = ADDn[MUL2(M ,ADD2(x,y)),MUL2(M ,SOUS(x,y)),MUL2(M ,MUL2(x,y)),MUL2(M ,DIVI(x,y)),MUL2(M ,MIN2(x,y)),MUL2(M ,MAX2(x,y)),...]
11 12 13 14 15 16
fSOUS(x,y) = ADDn[MUL2(M ,ADD2(x,y)),MUL2(M ,SOUS(x,y)),MUL2(M ,MUL2(x,y)),MUL2(M ,DIVI(x,y)),MUL2(M ,MIN2(x,y)),MUL2(M ,MAX2(x,y)),...]
21 22 23 24 25 26
fMUL2(x,y) = ADDn[MUL2(M ,ADD2(x,y)),MUL2(M ,SOUS(x,y)),MUL2(M ,MUL2(x,y)),MUL2(M ,DIVI(x,y)),MUL2(M ,MIN2(x,y)),MUL2(M ,MAX2(x,y)),...]
31 32 33 34 35 36
fDIVI(x,y) = ADDn[MUL2(M ,ADD2(x,y)),MUL2(M ,SOUS(x,y)),MUL2(M ,MUL2(x,y)),MUL2(M ,DIVI(x,y)),MUL2(M ,MIN2(x,y)),MUL2(M ,MAX2(x,y)),...]
41 42 43 44 45 46
(où les opérateurs {MIN2,MAX2} sont définis de façon naturelle au paragraphe 2.2.3)
avec par défaut pour les pondérations Mij (qui peuvent être variables...) :
M = 1 si i=j
ij
M = 0 si i#j
ij
D'autre part, en option, la soustraction et la division peuvent
être définies par :
x-y = x+(-y)
x/y = x*(1/y)
afin que les définitions courantes de l'addition et de la multiplication
contrôlent simultanément la soustraction et la division respectivement.
Si évidemment la plupart des possibilités ainsi offertes est sans
intêret, il en est au moins deux qui sont d'une grande utilité :
d'une part l'arithmétique "min-plus" où
l'addition et le multiplication sont remplacées respectivement par
le minimum et l'addition ce qui permet, par exemple,
à un produit matriciel "classique" de devenir un outil utile
en théorie des graphes, sans modification aucune de son code...
D'autre part, c'est aussi le cas de l'arithmétique "max-plus" (dite tropicale) où
cette fois-ci le maximum et l'addition remplace respectivement l'addition et la multiplication...
2.2-Définition du langage K :
Le langage K contient plusieurs classes de primitives que
nous allons très brièvement et non exhaustivement, loin
s'en faut, présenter par la suite, en notant
que contrairement au C "traditionnel", les
fonctions (aux sens C et K du
terme) ne peuvent qu'avoir
un nombre fixe d'arguments, et ceci pour améliorer encore davantage
la fiabilité des logiciels réalisés.
2.2.1-Définition des types
et des structures :
Toutes les fonctions nécessaires a la
définition de variables (valeurs scalaires,
tableaux, structures,...) existent et
permettent au moins ce
que permet le langage C. En fait plus de choses
sont autorisées dans ce domaine, et en particulier le
dimensionnement dynamique des tableaux ; par
exemple, C interdit généralement
l'écriture suivante (à l'exception du compilateur gcc en
certaines circonstances) :
int longueur=...;
int largeur=...;
(...)
float matrice[longueur][largeur];
alors que le K permettra toujours :
DEFV(Int,INIT(longueur,...));
DEFV(Int,INIT(largeur,...));
(...)
DEFV(Float,DTb2(matrice,longueur,largeur));
Cet exemple montre assez clairement la syntaxe utilisée ; il
convient d'ajouter que les symbôles
'Int' et 'Float' désignent eux-mêmes des fonctions,
dont l'un des arguments sera, après une
première passe de réécriture, l'objet qui est
défini à sa suite (par exemple 'longueur' ou encore
'matrice' ci-dessus). A celles-ci s'ajoutent des fonctions
d'accès aux structures, permettant comme
cela fut dit précédemment de masquer d'éventuelles
spécificités locales... Tous les types utiles
(entier, logique, réel,
complexe, quaternion, octonion,...
relatifs à des scalaires, des vecteurs, des
matrices,...) sont prédéfinis.
2.2.2-Définition des opérateurs
logiques :
Tous les opérateurs logiques nécessaires
sont disponibles, et par exemple :
IFLE(A,B) qui est VRAI si A <= B,
aIFEQ(A,B,E) qui est VRAI si A = B à E pres,
rIFEQ(A,B,P) qui est VRAI si A = B à P% pres,
INCL(C,A,B) qui est VRAI si C appartient à [A,B],
IFET(A,B) qui est VRAI si A est VRAI et si B est VRAI,
fIFET(A,B) qui est identique à IFET(A,B), mais pour la logique floue,
COND(C,A,B) qui vaut A si C est VRAI, et B sinon,
etc...
Bien entendu, ils sont tous récursifs.
2.2.3-Définition des opérateurs
arithmétiques :
Tous les opérateurs arithmétiques sont
disponibles, et par exemple :
ADD2(A,B) qui calcule A + B,
MUL2(A,B) qui calcule A * B,
MOYG(A,B) qui calcule la moyenne géométrique de A et B,
AXPB(A,B,C) qui calcule (A * B) + C,
MIN2(A,B) qui calcule le minimum de A et B,
MAX2(A,B) qui calcule le maximum de A et B,
MODG(C,A,B) qui ramène C dans le segment [A,B],
BARY(A,B,L) qui calcule ((1 - L) * A) + (L * B),
etc...
Là aussi, la récursivité est de rigueur...
2.2.4-Définition des structures
de contrôle :
Enfin, ces définitions permettent de
contrôler l'exécution des programmes. Se trouvent ainsi
définis, par exemple :
DoIn(indice,minimum,maximum,pas)
Bblock
< séquence éventuellement vide >
Eblock
EDoI
pour répéter un certain nombre de fois une certaine
séquence d'instructions,
Test(condition)
Bblock
< séquence éventuellement vide >
Eblock
ATes
Bblock
< séquence éventuellement vide >
Eblock
ETes
pour définir l'exécution conditionnelle de certaines
séquences d'instructions suivant le test
d'une condition logique,
Choi(expression recherchée quelconque)
Bblock
Case(expression 1 non nécessairement constante ou entière)
Bblock
< séquence éventuellement vide >
Eblock
ECas
(...)
Case(expression N non nécessairement constante ou entière)
Bblock
< séquence éventuellement vide >
Eblock
ECas
Defo
Bblock
< séquence éventuellement vide >
Eblock
EDef
Eblock
ECho
qui permet la prise de décision généralisée à choix
multiples ; il convient d'ajouter que d'une
part les séquences {Case,ECas} et {Defo,EDef}
peuvent être incluses, par exemple, dans des
séquences {Test,ETes}, et que d'autre part
les valeurs de sélection ne sont pas nécessairement
constantes et entières, contrairement au C.
Ces quelques exemples définissent bien entendu des structures
récursives ; celles-ci doivent
obligatoirement apparaître tel qu'elles sont ici
présentées, les séquences vides étant matérialisées à
l'aide d'un bloc vide ; c'est ainsi qu'apparaîtront très
fréquemment des séquences du type :
Test(condition)
Bblock
< séquence >
Eblock
ATes
Bblock
Eblock
ETes
2.3-Rédaction d'un programme et
enrichissement de l'environnement :
Le modèle de programmation et les outils présentés précédemment sont donc
utilisés pour rédiger des
programmes scientifiques (étude du chaos et de la turbulence,
génération d'objets fractals,
représentation d'états quantiques,...). Cette réalisation se
fera toujours en ayant présent à l'esprit les trois grands principes suivants :
- simplicité,
- généralité,
- harmonie.
S'ils peuvent paraître à priori inconciliables, l'expérience
accumulée ici montre qu'il n'en est rien.
Les très nombreux programmes tels qu'ils sont réalisés dans le
milieu de la recherche, par
exemple, à l'aide du Fortran, possèdent en général une
structure très "plate" :
-------- I
|-------- I
|-------- I
|-------- I
|-------- ...
|-------- ...
|-------- ...
P --------|-------- ...
|-------- ...
|-------- ...
|-------- ...
|-------- I
|-------- I
|-------- I
-------- I
où le programme P ne fait que référencer sur un seul niveau les primitives
I mises à la disposition du programmeur par le
langage de programmation utilisé. Bien sûr des fonctions et des sous-programmes
sont développés,
mais en général leur conception est telle, qu'elle ne favorise pas
leur réutilisabilité ; à titre
d'exemple, il est possible d'observer de façon très générale la
répétition, par l'utilisateur,
d'instructions ou de groupes d'instructions très voisines, voire
identiques. Nous proposons donc
une méthode aboutissant à une structure possédant beaucoup plus de niveaux et
dont les branches sont beaucoup plus equilibrées :
-------- I(1)
-------- I(2) -----|-------- ...
| -------- I(1)
-------- ... ... |-------- ...
| | -------- I(1)
| -------- I(2) -----|-------- ...
| -------- I(1)
-------- I(n-1) ---|-------- ...
| | -------- I(1)
| | -------- I(2) -----|-------- ...
| | | -------- I(1)
| -------- ... ... |-------- ...
| | -------- I(1)
| -------- I(2) -----|-------- ...
| -------- I(1)
P=I(n) ---|-------- ...
| -------- I(1)
| -------- I(2) -----|-------- ...
| | -------- I(1)
| -------- ... ... |-------- ...
| | | -------- I(1)
| | -------- I(2) -----|-------- ...
| | -------- I(1)
-------- I(n-1) ---|-------- ...
| -------- I(1)
| -------- I(2) -----|-------- ...
| | -------- I(1)
-------- ... ... |-------- ...
| -------- I(1)
-------- I(2) -----|-------- ...
-------- I(1)
I(1) désigne les primitives -les "atomes"- de plus bas niveau du langage
telles qu'elles sont mises à la disposition du programmeur, par exemple par le
langage K. Ensuite des instructions de
plus en plus complexes (au niveau des fonctionnalités, et non point
à celui de la syntaxe) I(i) -ou
"molécules"- sont élaborées avec un soucis constant de généralité et
de simplicité, pour arriver
finalement au niveau le plus élevé, où il est possible de qualifier
caricaturalement le programme P
d'instruction spécifique (puisque relative à un problème donné) I(n) de
plus haut niveau.
Ainsi, en respectant cette approche, la lecture du programme
pourra être réalisée à plusieurs
"échelles" ; plus celle-ci sera proche de la racine de l'arborescence évoquée
ci-dessus, plus la vue qu'en aura le
lecteur sera synthétique et plus la compréhension en sera globale et donc
meilleure. Les instructions
I(i) construites pour une application particulière seront placées
dans des bibliothèques (algèbre
linéaire, mécanique quantique, synthèse d'images,...)
afin d'être réutilisables ultérieurement ; ensuite, des utilitaires
en faciliteront l'accès et l'utilisation.
A titre d'exemple, les
figures "Visualisation tridimensionnelle de la dynamique de la superposition linéaire de 6 états propres de l'atome d'Hydrogène (calcul bidimensionnel)" et
"Visualisation tridimensionnelle de la dynamique de la superposition linéaire de 6 états propres de l'atome d'Hydrogène (calcul tridimensionnel)" montrent des
superpositions linéaires d'états propres de l'atome d'Hydrogène ;
celles-ci ont été calculées par le
programme
K déjà présenté, lui-même obtenu apres une heure de programmation en réutilisant les
développements faits pour l'étude des états propres et illustrés par la
figure "Visualisation tridimensionnelle de 36 états propres de l'atome d'Hydrogène (calcul bidimensionnel)"
(l'axe des ordonnées code le nombre quantique n (n appartient
à [1,8]), alors que celui des
abscisses code le nombre quantique l (l appartient à [0,n-1]).)...
3-ORGANISATION DE LA COOPERATION DANS UN ENVIRONNEMENT HETEROGENE :
C'est donc le langage K qui est utilisé pour
le développement de l'ensemble des logiciels. Voyons maintenant comment il permet d'obtenir la
meilleure utilisation de tous les moyens disponibles en inventoriant les différents types de
parallélisme et de coopération possibles qu'il supporte (ou est prêt à supporter) :
fonction(Arg1,Arg2,...,ArgN)
Implicitement, l'évaluation des arguments ArgI est réalisable en
parallèle ; deux fonctions de base du langage K :
SARG(fonction(Arg1,Arg2,...,ArgN))
et
PARG(fonction(Arg1,Arg2,...,ArgN))
permettent respectivement de contraindre l'évaluation séquentielle et le
retour au mode parallèle,
sachant que leur imbrication est possible ; par exemple,
l'écriture suivante est licite :
SARG(fonction(PARG(fonction1(Arg1,Arg2,...,ArgP))
,PARG(fonction2(Arg1,Arg2,...,ArgQ))
,...
,PARG(fonctionN(Arg1,Arg2,...,ArgR))
)
)
elle impose une évaluation séquentielle des fonctions fonctionI, et
simultanément permet,
pour chacune d'entre elles, que leurs arguments soient calculés
en parallèle.
Malheureusement, faute de machines accessibles permettant d'exploiter
ce micro-parallélisme,
les primitives PARG(...) et SARG(...) sont actuellement ineffectives.
3.2-La coopération homogène de type 1 :
Sur des systèmes d'architecture SIMD
(Single Instruction, Multiple Data), la définition de
certaines structures du langage K (les itérations
en particulier) pourraient contenir (conditionnellement donc, revoir
le paragraphe 1.4) des directives
de contrôle implicite d'un ensemble de processeurs. Ce n'est actuellement
pas le cas, faute de telles
machines facilement accessibles, mais il parait évident qu'une
telle adjonction pourrait être réalisée
sans trop de difficultés ; à titre d'exemple, la déclaration
(dynamique) des tableaux N-dimensionnels
(images,...), ainsi que leur accès, se faisant
par l'intermédiaire de fonctions, leur "éclatement" sur
un ensemble de processeurs ne poseraient pas de problèmes majeurs.
L'objectif est, bien entendu,
de ne pas remettre en cause les logiciels déjà réalisés (ou le moins possible...).
3.3-La coopération homogène de type 2 :
Le langage K contient un
certain nombre de
primitives de gestion et de coopération de processus (au sens UNIX du terme)
fonctionnant suivant
le principe "producteur-consommateur" ; elles sont en particulier adaptées
aux architectures MIMD
(Multiple Instruction, Multiple Data). Ainsi, un algorithme
décomposé en actions relativement
indépendantes pourra, si le découpage n'est pas trop fin,
être parallélisé explicitement. Certains
algorithmes de synthèse d'images, d'ailleurs beaucoup plus simples
à concevoir suivant une telle
logique, ont ainsi été programmés (et sont utilisés en fait
principalement sur des machines monoprocesseur...).
Il est ainsi possible d'écrire
(f1(), f2(), f3(), f4(), f5() et f6() étant
des fonctions arbitraires) :
f1(arguments);
/* 1-execution de la fonction f1, puis passage en mode parallele : */
PARALLELE(f2(arguments);
EMETTRE(valeurs);
f3(arguments);
);
/* 2.1-lancement sequentiel des fonctions f2 et f3, */
PARALLELE(f4(arguments);
RECEVOIR(valeurs);
f5(arguments);
);
/* 2.2-parallelement au lancement sequentiel des fonctions f4 et f5, avec */
/* echange de valeurs et synchronisation. */
WAIT();
/* 3-attente de fin des deux branches paralleles. */
f6(arguments);
/* 4-enfin retour au mode sequentiel pour l'execution de la fonction f6. */
Enfin, il convient d'ajouter qu'une réflexion est actuellement
en cours afin d'aboutir à un
portage sur une machine MIMD à mémoire distribuée (NCUBE 2S). Une
maquette de gestionnaire
de mémoire virtuelle distribuée a déjà été réalisée et plusieurs
algorithmes de génération d'objets
fractals ont été adaptés. Des mesures intéressantes ont ainsi pu être
effectuée quant à l'influence de
certains paramêtres (longueur des messages échangés, "granularité"
de la parallélisation,...).
Comme au paragraphe 3.2, l'objectif
serait de rendre invisible ces mécanismes en les insérant
(conditionnellement donc, revoir le
paragraphe 1.4) dans les
définitions appropriées du langage K.
3.4-La coopération hétérogène :
Ce qui a été ici réalisé s'inspire de la simplicité et de
l'élégance de Linda ; le "centre" du dispositif est un volume NFS partagé
entre tous les sytèmes
que l'on souhaite voir coopérer. A l'intérieur de celui-ci,
une arborescence A est créée
dynamiquement (au fur et à mesure que de nouvelles machines rentrent
dans le jeu...). Elle décrit
l'ensemble logique et physique des machines accessibles ; elle est destinée
à contenir des groupes de
commandes à exécuter. Schématiquement, elle possède la structure suivante :
A ------- Sq ------ P0 ------ f
| | |
| | --- b
| |
| |--- P1 ------ f
| | |
| | --- b
| |
| ...
|
|--- S1 ------ Mq ------ P0 ------ f
| | | |
| | | --- b
| | |
| | |--- P1 ------ f
| | | |
| | | --- b
| | |
| | ...
| |
| |--- M1 ------ P0 ------ f
| | | |
| | | --- b
| | |
| | |--- P1 ------ f
| | | |
| | | --- b
| | |
| | ...
| |
| ...
|
...
ou :
- Sq désigne un système logique quelconque (et par voie
de conséquence, des machines
physiques quelconques...),
- Si désigne un système logique d'un type donné (par
exemple 'VAX9000 sous ULTRIX
avec le compilateur gcc'),
- Mq désigne une machine physique quelconque pour un type
de système logique donné (par
exemple un VAX9000 quelconque sous ULTRIX avec le
compilateur gcc),
- Mj désigne une machine physique donnée d'un type de système
logique donné,
- Pk définit la priorité de rang 'k' ('0' étant la
plus élevée),
- f (foreground) et b (background) permettent de faire la
distinction entre l'exécution au
premier-plan et en arrière-plan.
Chaque machine physique Mj (dont le type de système logique est Si)
souhaitant coopérer
avec les autres (en offrant ses services) vient consulter,
par l'intermédiaire d'une tâche de fond
baptisée 'coShell', cette arborescence dans l'ordre des
priorités décroissantes suivant :
- la sous-arborescence Si/Mj, puis
- la sous-arborescence Si/Mq, et enfin
- la sous-arborescence Sq.
Pour chaque sous-arborescence et à partir de la priorité 0,
sont examinées les classes
foreground, puis background ; enfin à l'intérieur de chacune
d'elles, c'est l'ordre alphabétique qui
est utilisé (ce qui permet d'imposer un ordre d'exécution pour les
commandes équivalentes -au sens
de la classification opérée par l'arborescence A-). Les feuilles de
cette arborescence sont des
commandes (et plus généralement des "shell-scripts") à exécuter,
déposées là par les "clients"
(utilisateurs physiques, ou bien logiciels...) ; à chacune d'elles
se trouve associé un sémaphore
d'exclusion d'accès (dont le nom est celui de la commande précédé d'un
point -cette chaîne de
caractères est en fait un paramêtre modifiable, dont la valeur
implicite a été choisie en fonction de
l'utilisation de l'utilitaire 'll'-) situé bien évidemment au même
niveau de l'arborescence A.
Lorsque sur la machine Mj a été repérée la commande exécutable la
plus prioritaire
(relativement aux critères de parcours de l'arborescence), la
destruction du sémaphore associé est
tentée ; lorsque celle-ci est acceptée, la commande est
lancée (au premier-plan ou en arrière-plan
suivant le cas) sur cette machine ; dans le cas contraire, cela
signifie qu'il y a eu concurrence
malheureuse avec une autre machine, et que cette commande s'exécutera
donc ailleurs ; la machine
Mj reprend alors périodiquement sa recherche avec une fréquence qui est
une fonction décroissante
de sa charge courante. De plus, lors de l'exécution d'une
commande, cette dernière a le droit
d'interrompre son travail (après une erreur, ou bien encore
après la détection de l'absence d'un
certain fichier -commande, données,...- sur Mj,...)
et de se remettre en attente, là ou elle le juge
utile, dans l'arborescence A. Le chapitre 5 donnera un exemple
d'utilisation de ce concept.
Enfin, il convient de bien noter que ce modèle de la coopération
hétérogène présente un
avantage fondamental : les différents "participants" n'ont pas besoin de
connaître la configuration de
l'environnement (localisation des autres systèmes et leur nature,
topologie des connexions,
charge,...) ; bien entendu, ces informations sont
disponibles et exploitables, si besoin est. Il assure
de plus un êquilibrage dynamique de la charge des diffêrents
participants, puique la prise en compte
d'une activitê quelconque ne leur est pas imposêe, mais laissêe
à leur appréciation.
C'est à l'aide de cette méthode que sont calculées les images du type
"Rotation de 2.pi autour des axes Y et Z d'un ensemble de Julia dans le corps des quaternions -section tridimensionnelle-".
Malgré tout cela, une limitation possible de la coopération
hétérogène, relative aux erreurs d'arrondi,
sera présentée au paragraphe 4.2.3.
4-LA SENSIBILITE A LA PRECISION DES CALCULS :
L'environnement de programmation proposé permet de limiter les erreurs de programmation et de
modélisation. Il est malheureusement d'autres sources d'anomalies potentielles. Pour attirer
l'attention sur celles-ci, un certain nombre de résultats "dramatiques" vont être
présentées en
utilisant deux systèmes dynamiques classiques :
Sans vouloir offenser les numériciens, les sources d'erreurs qui seront ici
visualisées sont
souvent ignorées (faute de pouvoir les éviter ?). C'est ainsi que l'auteur d'un certain ouvrage
d'analyse numérique affirme "on supposera que les erreurs d'arrondi sont
négligeables", or cela est
faux dans certains cas (de très nombreux cas ?). Il est donc essentiel de
sensibiliser les chercheurs à
ce problème, tout en ne cachant pas cette difficulté aux étudiants. Nous
allons montrer que les
erreurs d'arrondis peuvent se manifester de plusieurs façons non exclusives l'une
de l'autre, et en particulier :
- lors des calculs arithmétiques proprement dits,
- lors de l'interprétation de la formulation informatique des
modèles (perte de l'associativité de
la multiplication et de sa distributivité par rapport a
l'addition, par exemple),
- lors de l'interprétation des constantes par les compilateurs,
- lors de l'entrée interactive des paramêtres,
- et enfin lors de coopérations entre systèmes hétérogènes.
- dx
| ---- = -10x + 10y
| dt
|
| dy
< ---- = 28x - y - xz
| dt
|
| dz 8
| ---- = - ---z + xy
- dt 3
Pour le résoudre numériquement, il est possible de procéder de
la manière suivante (méthode
d'Euler, schéma du premier ordre) : se donnant des conditions initiales
(x(0),y(0),z(0)), le
vecteur (x(i),y(i),z(i)) sera calculé itérativement
en fonction de la valeur (x(i-1),y(i-1),z(i-1)) qu'il possédait à
l'instant précédent grâce aux trois relations :
- x = x + Dx = x + (-10x + 10y )Dt
| i i-1 i-1 i-1 i-1
|
| y = y + Dy = y + (28x - y - x z )Dt
< i i-1 i-1 i-1 i-1 i-1 i-1
|
| 8
| z = z + Dz = z + (- ---z + x y )Dt
- i i-1 i-1 3 i-1 i-1 i-1
où 'Dt' représente le 'pas de temps' de la simulation. Il convient de
noter dès à present que lors
des expériences décrites ci-dessous, d'autres méthodes plus robustes
et plus stables (Runge-Kutta
du quatrième ordre, par exemple) ont été utilisées conjointement
au schéma d'Euler, et ont donné
lieu, bien entendu, au même type de phénomène,
éventuellement amplifié (ce qui, à la lumière de ce
qui va suivre, est tout à fait compréhensible, voir
le paragraphe 4.2.1). Les résultats obtenus sont
traditionnellement représentés sous forme de points
(x(i),y(i),z(i)) dans un espace tridimensionnel
(x,y,z) ; l'objet obtenu s'appelle l'attracteur de Lorenz
(voir L'attracteur de Lorenz).
L'histoire raconte que Edward Lorenz faisait imprimer par l'ordinateur les
résultats (x(i),y(i),z(i))
qu'il obtenait ; or un jour, il essaya de prendre comme état
initial, non pas le
(x(0),y(0),z(0)) programme
qu'il utilisait habituellement, mais plutôt un état intermédiaire
(x(m),y(m),z(m)) calculé et imprimé
antréieurement. Il eut alors une grande surprise : les valeurs suivantes
(x(m+j),y(m+j),z(m+j)) produites
par ce nouveau calcul s'éloignaient progressivement (et de plus en plus vite)
de celles obtenues
précédemment. Il comprit la cause de ce phénomène : les valeurs imprimées
par l'ordinateur ne
sont, en fait, que des valeurs approchées du contenu de sa
mémoire (arrondi et/ou précision
demandée insuffisante) ; par exemple 1.78 sera imprimé pour 1.789 (en
supposant un arrondi
décimal par défaut), or malheureusement, c'est 1.78 qui
sera réintroduit dans l'ordinateur
ultérieurement. Ainsi naissait "la sensibilité aux conditions
initiales", qui signifie donc que
le futur d'un système dynamique (tel celui qui fut décrit ci-dessus) ne
peut-être connu avec précision
à tous les instants du futur parce que son état initial ne peut être
fixé avec une précision
arbitrairement donnée. La variation la plus infime sera progressivement
amplifiée, et deux états
initiaux infiniment voisins s'éloigneront ensuite l'un de l'autre
inexorablement pour ainsi donner naissance à deux histoires totalement différentes
(voir la figure "L'attracteur de Lorenz -sensibilité aux conditions initiales (représentées par le point central de chaque image)-" ;
elle présente les 400 premières itérations de l'attracteur de Lorenz pour 16 jeux de
conditions initiales (x(0),y(0),z(0)) différentes qui peuvent être
observées facilement dans l'image en bas
à gauche : elles y sont représentées par le point central.
Ensuite, elles évoluent linéairement par pas de
0.01 de gauche à droite et de bas en haut. Les trajectoires
obtenues se ressemblent au cours des
premiers instants puis, ensuite, évoluent de façon
parfois tout à fait opposée.).
4.2-La sensibilité à la précision des calculs :
L'étude des systèmes dynamiques
montre que cette notion doit être étendue et renommée plus généralement
"sensibilité à la précision des calculs".
4.2.1-Les erreurs d'arrondis :
Reprenant le système
précédent, il est possible de
considérer tout vecteur (x(i),y(i),z(i))
comme étant l'état initial des itérations suivantes.
Si par malheur,
un ou plusieurs bits étaient perdus au cours des
calculs, ne devrait-on pas constater après chaque
itération un phénomène identique à celui qu'Edward Lorenz
avait observé il y a plusieurs années. Or
une telle dégradation peut apparaître en permanence.
En effet, la précision interne des
ordinateurs est limitée (en général 32 ou 64 bits pour
représenter les valeurs numériques ; mais
même avec une précision très supérieure, le phénomène
présentement décrit se manifesterait de toute
manière, mais un peu plus tard, puisqu'en
effet, le nombre de décimales nécessaires à ce type de
processus, croit en général exponentiellement avec le
nombre d'itérations) ; au cours de chaque
multiplication des chiffres sont perdus, or ils ont
tous dans ce contexte de l'importance (par exemple
3.14x2.71 donnera, en arrondissant à deux décimales
par défaut, 8.50, et non pas 8.5094).
Une remarque supplémentaire doit être faite alors : les
résultats obtenus dépendront de l'ordre
des opérations, et, en particulier, les
propriétés habituelles d'associativité de la multiplication et de
sa distributivité par rapport à l'addition, disparaîtront ;
par exemple, en simple précision (c'est-à-dire sur 32 bits
et ce afin de simplifier l'impression hexa-décimale des résultats) :
(3.1415 * 2.7182) * 1.2345 = 0x4128AAB3
#
alors que :
3.1415 * (2.7182 * 1.2345) = 0x4128AAB2
#
La façon de rédiger le programme informatique (au niveau des
parenthèses en particulier), de
même que le compilateur utilisé (et les options
choisies, en particulier au niveau de l'optimisation)
joueront donc un rôle très important quant aux valeurs des
résultats obtenus (ce point joue un rôle
essentiel dans la méthode que nous proposerons au
paragraphe 4.3 et
sera illustré au paragraphe
4.2.2 à l'aide de la dynamique de Verhulst).
Cela implique alors des conséquences au niveau de la
portabilité des programmes,
puisqu'en effet, tous les ordinateurs
(sous-entendu les couples {matériel,logiciel}) ne
représentent pas et ne manipulent pas les nombres de la même
façon et dans le même ordre ; dans
ces conditions, deux calculs formellement identiques
(le programme correspondant ayant même pu
être validé à l'aide d'outils de preuve automatique...),
mais effectués sur deux machines différentes
pourront donner des résultats différents, qui dans le
cas d'un système dynamique pourront
s'éloigner exponentiellement l'un de l'autre, les
prévisions relatives au devenir de l'état du
système étudié dépendant fortement de l'outil de calcul !
Faut-il alors introduire la notion
de subjectivité numérique ou encore celle de
relativité numérique ?
Ce phénomène peut s'observer en étudiant l'attracteur de
Lorenz. Un même programme en K
(le phénomène est évidemment indépendant du langage utilisé...)
est exécuté sur trois machines
différentes : un IBM RS6000, un IBM ES9000 et enfin
un Silicon Graphics INDIGO XS24 (ces
machines ont été choisies car elles sont architecturées
différemment et/ou ne disposent pas d'un
compilateur de même origine). Trois méthodes différentes
d'intégration numérique ont été testées :
Euler d'ordre 1,
Runge-Kutta d'ordre 2 et
Runge-Kutta d'ordre 4 ;
c'est cette dernière qui sera ici
présentée (les deux premières manifestant bien entendu le
même comportement), car elle est, des
trois, la plus stable et la plus précise
mathématiquement parlant ; il convient de remarquer
ici que, paradoxalement, les méthodes d'ordre
élevé faisant beaucoup plus de multiplications que celles
d'ordre faible, risquent d'être, dans ces
circonstances, moins "performantes" au niveau
numérique.... Enfin, notons qu'en fait,
peu importe la méthode, fut-elle
médiocre, voire erronée ;
en effet le phénomène mis en lumiere ici est le désaccord
entre plusieurs ordinateurs relativement à
un même programme, qu'il soit bon ou mauvais
(suivant un certain critère arbitraire)...
Les résultats obtenus sont superposés en utilisant un code
de couleur très simple (s'inspirant
du système additif de synthèse des couleurs -le RVB-) : le
Rouge est associé à la première machine,
le Vert à la seconde et le Bleu à la troisième. Les images
présentées sur la
figure "Calcul de l'attracteur de Lorenz sur trois ordinateurs différents (le Rouge, le Vert et le Bleu : sensibilité aux erreurs d'arrondi)"
prédisent l'évolution du système pour seize instants répartis
régulièrement au cours des
itérations 4180 à 5380 ; les calculs furent effectués en
double précision avec comme conditions
initiales (x(0),y(0),z(0))
= (0.01,0.01,0.01) et un pas de
temps 'Dt' de 0.01.
Ainsi qu'il était possible de l'espérer, au début
des calculs (jusqu'aux environs de l'itération
4700), les trois machines sont en accord les unes
avec les autres (les erreurs sont trop faibles pour
être visibles sur les images, ce qui se traduit par
des points blancs résultant de la superposition des
trois couleurs fondamentales). Puis,
progressivement, les résultats se séparent
tout en semblant proches l'un de l'autre ; mais aux environs
de l'itération 4980, la couleur Vert se sépare
brutalement et complètement du Rouge et du Bleu ; l'une des
trois machines n'est alors plus du tout en accord
avec les deux autres. Enfin, vers l'instant
5060, le Rouge et le Bleu s'échappent l'un de l'autre.
Notons qu'au cours de ces expériences, d'autres
ordinateurs furent aussi utilisés (DEC VAX
9000, Silicon Graphics 4D20,
Sony NWS 3260,...). Avec certains (par exemple
un Alliant FX2800) la sensibilité aux conditions initiales
est venue compliquer l'analyse de la situation ; en
effet, pour des raisons inutiles à détailler
ici, les constantes 0.01, par
exemple, sont formulées
((1.0/10)/10) ; pour le compilateur 'scc' de cet
ordinateur, cette dernière expression diffère au
niveau de son approximation binaire, très
légèrement, de celle de 0.01, ce qui n'était
pas le cas des
autres couples {machines,compilateurs} testés...
Soulignons bien que deux points de couleurs différentes,
mais voisins dans l'espace des
phases (x,y,z), correspondent en
général, sauf dans les premières
itérations, à des instants très
différents. Ainsi, pour un instant quelconque
suffisamment éloigné de l'origine temporelle du
calcul, les prédictions effectuées sont en complète
contradiction les unes avec les autres, même si la
forme de l'attracteur est conservée. Malgré tout,
il convient de noter que ceci ne sera absolument pas
satisfaisant, lorsque l'objectif sera de faire des
prévisions quant au devenir du système à partir de
conditions initiales fixées, puisque pour un instant
donné du futur, les états calculés diffèrent
complètement d'une machine à l'autre.
Une conséquence inévitable de ce phénomène est alors la
sensibilité à la méthode
d'intégration numérique. En effet, en ce qui concerne
les équations dites non intégrables, une
méthode numérique doit être utilisée pour obtenir une valeur
approchée de leurs solutions. Il en
existe de très nombreuses (Euler -revoir le
paragraphe 4.1-,
Runge-Kutta,...) ; étant toutes très
différentes, et en particulier ne demandant pas les
mêmes opérations arithmétiques (en nombre et en
ordre), il est alors évident que les résultats
dépendront aussi de la méthode utilisée pour les obtenir
(voir la figure "L'attracteur de Lorenz -sensibilité à la méthode d'intégration utilisée (Rouge=Euler, Vert=Runge-Kutta/second ordre, Bleu=Runge-Kutta/quatrième ordre)-").
Alors quelle machine a raison ? Malheureusement
aucune, et chercher, par exemple, N
ordinateurs d'accord entre eux, ne donnerait
pas, bien évidemment, le bon résultat... La
connaisance du véritable attracteur de Lorenz semble
échapper à nos moyens de calculs et à nous par
la-même occasion... Ce système dynamique n'est pas une
exception ; d'autres exemples, tout aussi
simples, voire beaucoup plus élémentaires
(ne demandant pas en particulier d'intégration
numérique), et présentant le même
comportement, peuvent être exhibés, ainsi
que cela sera montré dans ce qui suit...
4.2.2-La formulation informatique
du modèle :
La dynamique de Verhulst modélise par l'itération :
2
X = (R+1)X - RX
n n-1 n-1
(où la valeur de X0 est donnée) la variation d'une
population d'individus. Ici, plus question
d'intégrer des équations différentielles à l'aide de
méthodes numériques (et donc approximatives) :
le calcul à effectuer est simple, "parfait" et
semble pouvoir être effectué sans difficultés, comme
certains ouvrages sembleraient vouloir le faire croire...
Pour R >2.57, cette itération présente un
comportement dit "chaotique".
Nous allons vérifier que la multiplication n'est plus ici
ni associative, ni distributive par rapport
à l'addition ; par exemple :
X = ((R+1)*X) - (R*(X*X));
et
X = ((R+1)*X) - ((R*X)*X);
ne sont pas, dans ce contexte,
deux écritures équivalentes, loin s'en faut. En fait
cinq familles de formulation de l'itération précédente ont été
mises en évidence (tout autre forme possible se
ramenant, quant aux résultats numériques,
à l'une de ces cinq formes). Ainsi que le montre le
tableau suivant, elles donnent, chacune
dans les mêmes conditions (programme double précision en
C, avec R=3.0 ; voir à ce propos le
programme utilisé),
cinq jeux de résultats complètement incompatibles
entre eux après un nombre très faible d'itérations (moins de 100) :
IBM ES9000 :
(R+1)X-R(XX) (R+1)X-(RX)X ((R+1)-(RX))X RX+(1-(RX))X X+R(X-(XX))
X(00) = 0.500000 0.500000 0.500000 0.500000 0.500000
X(10) = 0.384631 0.384631 0.384631 0.384631 0.384631
X(20) = 0.418895 0.418895 0.418895 0.418895 0.418895
X(30) = 0.046399 0.046399 0.046399 0.046399 0.046399
X(40) = 0.320185 0.320183 0.320188 0.320182 0.320189
X(50) = 0.063406 0.064521 0.061895 0.064941 0.061244
X(60) = 1.040381 0.846041 0.529794 1.319900 1.214070
X(70) = 0.004104 1.199452 0.873553 0.573637 0.000009
X(80) = 0.108044 0.121414 1.260726 0.395871 0.280590
X(90) = 0.096374 0.089244 0.582157 0.344503 1.023735
IBM RS6000 :
(R+1)X-R(XX) (R+1)X-(RX)X ((R+1)-(RX))X RX+(1-(RX))X X+R(X-(XX))
X(00) = 0.500000 0.500000 0.500000 0.500000 0.500000
X(10) = 0.384631 0.384631 0.384631 0.384631 0.384631
X(20) = 0.418895 0.418895 0.418895 0.418895 0.418895
X(30) = 0.046399 0.046399 0.046399 0.046399 0.046399
X(40) = 0.320177 0.320184 0.320188 0.320190 0.320189
X(50) = 0.067567 0.063747 0.061859 0.060822 0.061486
X(60) = 0.001145 0.271115 0.616781 0.298613 1.307350
X(70) = 1.296775 1.328462 0.486629 0.938605 1.054669
X(80) = 0.553038 0.817163 1.277151 1.325437 0.617058
X(90) = 0.094852 0.154184 1.174162 0.148151 0.237355
Il convient de noter qu'un même algorithme sensible à ce phénomène et transcrit en Basic,
en C,
en Fortran,
en python ou encore à l'aide
d'un tableur (Excel par exemple), donnera évidemment,
dans tous les cas, les mêmes graves anomalies.
Il serait possible de trouver regrettable que, pour
une même forme mathématique, les compilateurs puissent
générer plusieurs programmes exécutables différents ; en tout
cas, cela présente au moins l'avantage
de mettre en évidence le phénomène et la fausseté des
résultats (ci-dessus aucune colonne n'est la
bonne, et cela resterait vrai même s'il n'en restait
qu'une seule).
Ces deux tableaux pourraient laisser croire que le programmeur
maîtrise malgré tout quelque chose, puisque de ses choix
(en matière de parenthésage par exemple)
dépendent les résultats numériques obtenus.
En fait il n'en est rien, ainsi que le montrent
les expériences qui suivent.
L'optimisation du code généré (en général optionnelle),
et de manière plus générale sa manipulation
(en général incontrôlable par l'utilisateur) lors de la phase
de compilation aura, elle-aussi, son mot à
dire en ce qui concerne les résultats (l'une des conséquences
de cela est que, lors de l'installation
d'une nouvelle version -release- d'un système, les
compilateurs changeant alors en général,
d'anciens programmes, recompilés dans ce nouveau
contexte, pourront produire des résultats très
différents de ceux obtenus antérieurement...).
A titre d'exemple, voici les résultats obtenus
après compilation et exécution du programme procédant à
l'itération suivante :
X = (R+1)*X - R*X*X;
sur une O200 Silicon Graphics (processeur R10000),
sous IRIX 6.4 et cc 7.1,
et suivant deux options d'optimisation différentes :
O200 Silicon Graphics (R10000, IRIX 6.5.5m, cc 7.2.1) :
option '-O2' option '-O3'
X(00) = 0.500000 0.500000
X(10) = 0.384631 0.384631
X(20) = 0.418895 0.418895
X(30) = 0.046399 0.046399
X(40) = 0.320184 0.320192
X(50) = 0.063747 0.059988
X(60) = 0.271115 1.000531
X(70) = 1.328462 1.329692
X(80) = 0.817163 0.021952
(il conviendra de noter que pour mettre ce phénomène en
évidence il est nécessaire d'éviter les parenthèses
redondantes dans la programmation de l'itération et
de l'écrire tel qu'elle apparait ci-dessus).
La figure "Intégration du problème des N-corps (N=4 : une étoile, une planète lourde et une planète légère avec un satellite) calculé avec 2 options d'optimisation différentes (sensibilité aux erreurs d'arrondi)" donne un
autre exemple de ce phénomène.
Ceci amène à faire une constation pénible : dans certaines
circonstances (et c'est le cas ici), il peut être
impossible (ou dangereux) d'implémenter informatiquement une méthode
numérique, puisque les compilateurs jouent un rôle
non négligeable et non maîtrisable quant aux résultats
produits...
Augmenter la précision des calculs (les exemples précédents
ont été obtenus, rappelons-le,
avec des programmes écrits en double précision
-64 bits-), tout en étant extremêment coûteux,
en particulier au niveau du temps de calcul, ne fait
que retarder de quelque peu l'apparition du phénomène.
Ainsi, des expériences ont été faites
avec bc
("arbitrary-precision arithmetic language"). Le tableau suivant
donne le numéro d'itération n
à partir duquel la partie entière est fausse,
en fonction du nombre de décimales utilisées pour
faire le calcul (paramêtre scale) :
scale = 010 020 030 040 050 060 070 080 090 100
n = 035 070 105 136 169 199 230 265 303 335
Il est évident que le gain obtenu ne peut justifier
le coût de cette opération. Construire des ordinateurs utilisant
une précision très supérieure, ne semble donc
pas être la solution à ce problème, or nous disposons
de machines toujours plus rapide, grâce auxquelles,
pour un problème donné, nous pouvons faire davantage
d'itérations...
Utiliser l'arithmétique dite
d'intervalle, semble
aussi voué à l'échec sur les problèmes sensibles aux conditions initiales
(comme l'est l'exemple précédent). En effet, un intervalle
[x-epsilon,x+epsilon] encadrant une valeur exacte ne peut,
en toute généralité, qu'"exploser" au cours
des calculs suivants,
puisque 'x-epsilon' et 'x+epsilon' peuvent être considérées
comme deux conditions initiales voisines...
Notons enfin que les microprocesseurs actuels ont de plus
en plus tendance à
réordonner, au niveau matériel, l'ordre des
opérations (tel qu'il est défini dans un programme
exécutable) en fonction des ressources disponibles et afin
d'optimiser l'utilisation de ces dernières
(voir par exemple les architectures dites super-scalaires).
Dans ces conditions, un même
"exécutable" réexecuté plusieurs fois, sans aucune
modification, pourrait donner des résultats
différents tout simplement parce qu'il est pratiquement
impossible de se retrouver deux fois de suite
dans exactement la même situation (penser, par
exemple, aux interruptions qui constituent des
événements aléatoires et aux multiplications à trois facteurs)...
4.2.3-Note sur le parallélisme
hétérogène :
Ainsi que cela fut décrit au chapitre 3, les
outils que l'informatique met aujourd'hui à notre
disposition, permettent de mettre en place des
méthodes de calcul à parallélisme hétérogène. Pour les modèles
dynamiques manifestant un tel
comportement chaotique, deux types de
"disfonctionnements" sont donc alors à prevoir :
- des problèmes liés aux erreurs d'arrondi
locaux à chacune des machines (c'est-à-dire
ceux qui furent décrits ci-dessus),
- mais aussi, des problèmes d'incohérence
entre les erreurs d'arrondi sur des machines
d'architecture et de compilateur
différents, lors d'activités réparties.
Pour illustrer ce dernier point, reprenons
l'exemple de l'attracteur de Lorenz. Même si dans le
cas présent cela présente peu d'intérêt au niveau des
performances, supposons qu'au lieu de calculer
trois fois le vecteur (x(i),y(i),z(i))
sur trois machines différentes, nous calculions
x(i) sur la première, y(i)
sur la seconde et enfin z(i) sur la troisième (avec envoi
de la valeur calculée par une machine aux deux
autres, bien entendu). Les erreurs commises alors sur
chacune des trois coordonnées ne seraient pas
de même "nature" (nous supposons bien entendu que les trois
couples {ordinateur,compilateur}
différent). La trajectoire du système alors calculée serait
encore différente des trois obtenues
précédemment ; la forme de l'attracteur serait-elle
conservée cette fois-ci ? L'expérience reste à faire...
Une expérience intéressante a déjà été réalisée : elle consiste
à faire calculer (par la méthode décrite au
paragraphe 3.4)
par deux ordinateurs différents (au niveau
logiciel et/ou matériel) une animation présentant une rotation
autour de l'axe 'OX' de l'attracteur de Lorenz. Chacune des
images est calculée par une machine et une seule. Nous voyons
ici le résultat pour 1000 itérations
et 5000 itérations
. Dans le premier cas,
le fait de n'avoir effectué que peu d'itérations rend les
erreurs d'arrondi "invisibles" (l'animation est cohérente),
alors que dans le second cas, elles se manifestent,
rendant l'animation incohérente...
Enfin, dans ce contexte et pour des raisons
évidentes, un tel calcul ne peut être commencé sur
une machine et terminé sur une autre (en cas de panne par
exemple...) à moins d'avoir pris des
précautions inspirées de la méthode exposée au
paragraphe suivant.
4.3-Une méthode élémentaire de détection
de la sensibilité à la précision des calculs
par réordonnancement des expressions arithmétiques :
Afin de mettre en évidence très rapidement d'une part l'influence de la
formulation informatique d'un problème en double précision, et
d'autre part les incompatibilités arithmétiques entre deux machines (unités de
calcul et/ou compilateurs), nous proposons d'utiliser le
programme (C ou Fortran) d'étude de la
dynamique de Verhulst (voir l'annexe 3) ; très court, il peut
être introduit dans n'importe quel
système en quelques secondes. Deux étapes se révèlent alors nécessaires :
la première qui indique, en
particulier, si la méthode proposée est applicable sur un ordinateur
donné et pour un certain
compilateur, et la deuxième qui montre, lorsque cela a été
rendu possible par la première etape, si le
problème testé est sensible à la précision des calculs (et aussi
simultanément, bien entendu, à celle
des conditions initiales -revoir le paragraphe 4.1-).
1-Première étape :
elle donne, en exécutant le
programme d'étude de
la dynamique de Verhulst cite précédemment, les
informations suivantes :
- si, à numéro d'itération fixé
(supérieur ou égal à 60), les cinq
colonnes de valeurs sont
différentes sur une machine donnée,
c'est que le compilateur utilise fait dépendre
le code
généré de la formulation informatique du
problème (cela semble être le cas général...).
- si, en effectuant ce même calcul sur
deux machines différentes, les deux
ensembles de
résultats diffèrent, c'est que,
soit les deux unités arithmétiques ne fonctionnent
pas de manière
semblable (en ce qui concerne la représentation
des nombres flottants et/ou leur
manipulation), soit
les deux compilateurs utilisés analysent
différemment les expressions (ces deux
conditions n'étant
pas exclusives l'une de l'autre...). Ainsi sont
mis en évidence les risques d'incohérence entre
deux ou plusieurs machines.
2-Deuxième étape :
ayant effectué la première étape, et si le compilateur
utilisé sur une
machine donnée fait dépendre le code généré de la forme des
expressions programmées, une
méthode très simple de détection de la sensibilité d'un
programme à la précision des calculs peut être
proposée ; elle exploite le fait que dans ces
circonstances, les propriétés habituelles de la
multiplication (associativité et distributivité par rapport
à l'addition) ne sont plus respectées. Elle consiste alors à :
- repérer dans ce programme les zones
"sensibles" (c'est-à-dire les séquences de
code, en
général très localisées, où les
processus itératifs et non linéaires
sont mis en œuvre),
- réécrire de deux ou trois façons différentes
(voir le
paragraphe 4.4)
celles-ci, par exemple en
réordonnançant les multiplications à plusieurs
facteurs, ou encore en faisant jouer la
distributivité de
la multiplication par rapport à l'addition
(une autre façon de procéder, mais plus
difficilement
contrôlable, consiste à compiler
plusieurs fois le même programme avec des options
d'optimisation différentes),
- exécuter le programme avec les mêmes paramêtres
et les mêmes conditions initiales suivant les
quelques modes définis ci-dessus,
- et enfin, comparer les résultats
obtenus et leur précision...
Cette méthode, sans pénaliser aucunement l'exécution
du programme (si ce n'est qu'il doit être
exécuté plusieurs fois), ni augmenter son
encombrement mémoire, implémenté tout naturellement
les méthodes de détection à base de perturbation aléatoire
du dernier bit (Jean Vignes et Françoise Chatelin).
Elle est enfin applicable à des modèles statiques
(contrairement à celles qui sont utilisées
dans les tests de la sensibilité aux conditions
initiales, et qui reposent sur la comparaison des
résultats obtenus en faisant varier le pas de temps).
Une remarque très importante doit être faite ici :
il apparait que, d'après les tests que nous avons
effectués, ces dépendances ne se manifestent en
toute généralité qu'en double précision, mais il
ne s'agit pas là d'une véritable limite de la
méthode proposée, puisqu'en effet, la
plupart des codes de calcul numérique utilisent ce mode de
fonctionnement et que dans le cas contraire rien n'interdit en
général le passage de la simple à la double précision...
4.4-Contribution du langage K
au test de la sensibilité d'un problème à la
précision des calculs :
L'environnement de programmation que nous proposons simplifie
considérablement le test précédent. En effet, prenons
l'exemple d'une multiplication à trois facteurs du type :
A*B*C
('A', 'B' et 'C' désignant trois expressions quelconques).
En K, elle sera notée 'MUL3(A,B,C)' et définie de la façon suivante :
MUL3(A,B,C) ==> MUL2(A,MUL2(B,C))
De même, l'expression générale :
Ax(B + C)
se notera 'DIS2(A,B,C)' et sera définie par :
DIS2(A,B,C) ==> MUL2(A,ADD2(B,C))
(où 'ADD2(,)' et 'MUL2(,)' représentent respectivement,
rappelons-le, l'addition et la
multiplication de deux expressions quelconques).
Le langage K étant redéfinissable à tout moment,
et même à l'intérieur d'un programme, il est
possible de faire, dans ce cas, que tous les produits à trois
facteurs 'MUL3(A,B,C)', dans une
certaine zone du programme, deviennent par exemple :
MUL2(B,MUL2(C,A))
(ou tout autre forme licite) en jouant a priori simultanément sur
l'associativité et la
commutativité de la multiplication. De même,
'DIS2(A,B,C)' pourra être redéfinie par :
ADD2(MUL2(A,B),MUL2(A,C))
en jouant a priori sur la propriété de distributivité de la multiplication
sur l'addition.
Le dernier fichier interprété par le traducteur K
(avant de s'attaquer à la compilation
proprement dit) est celui qui est destiné à contenir, entre autres
choses, ce type de redéfinition ; il est vide par défaut...
Ainsi, sans avoir à reprogrammer certaines zones (avec les risques
encourus de mauvaise
reformulation du problème, ce qui en général se verrait alors des
les premières itérations...),
automatiquement, il est possible avec le K de
produire un code généré différent, sans avoir à
changer la forme des expressions contenues dans les programmes. Ceci
présente le double avantage
de la sécurité et de la rapidité... Notons qu'une telle facilité pourrait
être introduite relativement
facilement dans les compilateurs "classiques" : une option permettrait de
choisir, lors de l'analyse
des expressions, un type de "bouleversement" de l'ordre des
opérations, suivant une ou plusieurs
des réécritures évoquées ci-dessus ; enfin, rappelons que,
dans bien des cas, cela peut être simulé
dès à présent en jouant tout simplement sur les différentes options
d'optimisation (tout controle échappant alors à l'utilisateur).
4.5-Une information nécessaire :
Il est fort probable que les systèmes dynamiques
beaucoup plus complexes que ceux que nous avons utilisés ci-dessus à
titre démonstratif, et en
particulier ceux de la prévision météorologique à long terme (dont la
sensibilité aux conditions initiales est bien connue)
ou encore ceux de l'étude de la stabilité du système solaire
(voir "Intégration du problème des N-corps (N=4 : une étoile, une planète lourde et une planète légère avec un satellite) calculé sur trois ordinateurs différents (le Rouge, le Vert et le Bleu : sensibilité aux erreurs d'arrondi)"),
dépendent des ordinateurs et de la forme des programmes qui permettent
de les étudier ! Il est donc impératif de détecter un tel comportement
et de bien connaître leurs
"limites" (lors d'études de paléo-climatologie, de l'effet de
serre, ou bien encore de l'hiver
nucléaire). Malheureusement, la méthode très simple que nous
proposons ci-dessus, ou encore celle
que propose par exemple Jean Vignes et Françoise Chatelin, ne
font qu'alerter l'utilisateur sur les
risques encourus (en lui fournissant des indications sur la
précision disponible), mais en aucune
manière, elles ne donnent le bon résultat.
Aujourd'hui, ou notre recherche, tant fondamentale
qu'appliquée, repose en grande partie sur
l'outil informatique, il est essentiel que d'une part,
ses utilisateurs présents et à venir (chercheurs,
ingénieurs et étudiants) soient tous parfaitement conscients de ce
danger, et que d'autre part un
effort soit entrepris vers la recherche de solutions concrètes à ce
réel problème, si tant est qu'elles
existent. Des résultats théoriques ont déjà été exhibés ;
malheureusement, ils n'étudient que
quelques systèmes particuliers (l'attracteur de Hénon,
par exemple), et montrent qu'il y a
toujours, sur une durée finie, une orbite réelle voisine d'une orbite
numériquement bruitée (ce que les images
précédentes laissent supposer pour l'attracteur de Lorenz), mais
ce au prix d'un changement des
conditions initiales, ce qui interdit l'étude du devenir d'un
système de conditions initiales données...
5-UN EXEMPLE D'UTILISATION DES CONCEPTS :
L'ensemble des librairies de gestion de l'environnement, de calcul,
de visualisation,... a été programmé en langage K.
Il en est de même de tous les programmes nécessaires à la réalisation de tout
ce qui est présenté sur ce site. Fin 2017, cela représentait
plus de 2.600.000 lignes de codes. Présentons maintenant un exemple concret...
Lors de la présentation du langage K
(au paragraphe 2.1.8),
un film sur la structure en quarks des nucléons (protons et neutrons) a été évoqué.
Longtemps considérés comme élémentaires, les nucléons
(le proton et le neutron),
sont aujourd'hui décrits en tant qu'objets composés. Le modèle,
dit "standard", des particules et de
leurs interactions repose sur les bosons (les "vecteurs des forces") et
les fermions (la "matière") ;
dans cette dernière catégorie se trouvent les leptons et les quarks.
Cette image nous montre une vue
d'un nucléon tel qu'il est défini dans ce modèle : il est constitué de trois
quarks dits "réels" (ou de
valence) localisés approximativement aux sommets d'un triangle équilatéral assez
apparent sur cette
image ; ils sont respectivement porteurs d'une charge de couleur rouge,
verte et bleue (cette charge est
naturellement représentée à l'aide des couleurs plus familières et de même nom).
Une "mer" de particules virtuelles (quarks et anti-quarks -objets sphériques-,
gluons -objets allongés-) nées des
fluctuations quantiques du vide et des interactions, remplit le "volume" du nucléon.
Nous allons maintenant décrire brièvement l'architecture du
logiciel permettant de réaliser la simulation des processus correspondants. Il
est constitué de trois modules :
- le Simulateur S,
- le Générateur G,
- l'Enregistreur E,
que nous allons détailler, en notant au préalable que, bien
entendu, G et E sont complètement
indépendants de S, et sont donc utilisés pour la production générale de nos films.
5.1-Le Simulateur S :
Il fonctionne suivant l'algorithme
très simplifié suivant (ou
l'évolution calculée pour le dernier instant est inutilisée) :
Bblock
CALS(initialisations());
DoIn(temps,instant_de_debut,instant_de_fin,pas_de_temps)
Bblock
CALS(generer_un_shell_script(temps));
CALS(evolution_du_système(temps));
Eblock
EDoI
CALS(fin());
Eblock
ou 'generer_un_shell_script()' est une fonction qui range dans
l'arborescence A
un fichier de type 'shell-script' dont le nom contient le numéro de
l'instant courant
(complété à gauche par un
nombre suffisant de zéros, afin que les ordres alphabétique et
chronologique coïncident) et qui
contient d'une part les diverses caractéristiques géométriques
des particules à
visualiser, et d'autre part l'appel à la commande de
génération G des images.
Dans l'état actuel des choses, S est séquentiel,
c'est-à-dire
qu'il n'utilise pas, par exemple, de
directives du type 'PARALLELE()'. Le simulateur est a priori
situé sur une seule
machine (mais quelconque, de par la portabilité absolue
assurée par le
langage K), et son lancement (seule
opération "manuelle" à réaliser...) consiste à placer la
commande correspondante quelque part dans
l'arborescence A (et par exemple dans la sous-arborescence Sq
si la machine de
calcul importe peu à cet instant...).
5.2-le Générateur G :
Ce logiciel est chargé de générer une image (et une seule) du film
pour laquelle il reçoit les caractéristiques des particules à visualiser
(position, vitesse, forme,
couleur,...). La figure "Structure en quarks et gluons du nucléon"
montre
une intégration temporelle de trente-deux telles images (pour un pas
de temps de l'ordre de 10e-25 seconde). G est dupliqué sur toutes les
machines utiles à la production
du film, et est activé sur chacune d'elles suivant le principe
décrit au chapitre 3. Ainsi à un instant
donné, plusieurs images, correspondant à des instants
différents du film pourront être en cours de
génération ; si l'ordre alphabétique a été choisi identique à l'ordre
chronologique, c'est afin de
garantir que les images correspondant à des instants voisins de T soient
calculées avant celles qui
correspondent au voisinage de T+DT > T, et
qu'ainsi l'enregistreur E attende le moins possible (il
serait par exemple absurde que les images 100, 101 et 102
soient déjà calculées, alors que les
images 80, 81 et 82 ne le seraient pas encore, et
donc attendues par E de manière bloquante...).
Chaque instance du générateur G produit un fichier image dont le
nom contient là aussi le numéro
de l'instant courant, et qui est stocké dans un volume NFS
réservé à cet usage...
5.3-l'Enregistreur E :
Ce programme ne peut s'exécuter que sur une machine physique
disposant d'un enregistreur vidéo. Il récupère séquentiellement
(c'est-à-dire dans l'ordre des
instants croissants) les images là où elles sont stockées,
les affiche à l'aide d'un imageur, puis enfin
pilote un disque vidéo. Il convient de noter que la contrainte
d'enregistrement séquentiel des images
(qui a rendu nécessaire l'identité des ordres chronologiques
et alphabétiques) n'existe que par le fait
qu'avant chaque enregistrement d'une nouvelle image, est
diffusé l'ensemble de celles qui ont déjà
été enregistrées (et ce afin de vérifier en permanence le bon
comportement du modèle) ; lorsque ce
dispositif est inhibé, l'enregistrement peut être,
bien entendu, aléatoire.
6-CONCLUSION :
Tous les concepts précédents (d'une nature fondamentalement
pragmatique, rappelons-le) fonctionnent aujourd'hui parfaitement sur tous les
ordinateurs UNIX de
l'Ecole Polytechnique. Ainsi des systèmes hétérogènes perçus, programmés et
commandés strictement de la même façon peuvent coopérer de façon efficace, en se
répartissant au mieux le travail à effectuer, tout en conservant la possibilité
de profiter des
spécificités locales. Enfin, la méthodologie de programmation présentée permet
de développer
rapidement des logiciels plus fiables, facilement maintenables, aisément
portables, et dont la sensibilité à
la précision des calculs peut être testée.
Copyright © Jean-François COLONNA, 1995-2024.
Copyright © France Telecom R&D and CMAP (Centre de Mathématiques APpliquées) UMR CNRS 7641 / École polytechnique, Institut Polytechnique de Paris, 1995-2024.