OVH Cloud OVH Cloud

Calculs du point de vue de la norme

57 réponses
Avatar
jul
Salut,

Je programme une application qui fait un fort usage des calculs avec
des valeurs d=E9cimales (=E0 virgule) avec les types float ou double. Il
ne me semble pas que ces calculs sont pris directement en charge par
le(s) processeur(s). Ma question est donc la suivante : la norme
impose-t-elle l'identit=E9 des r=E9sultats de calculs entre diff=E9rents
compilateurs ? Si oui, au moyen de quel(s) crit=E8re(s) ?

Je pr=E9cise pour bien me faire comprendre. Il facile d'exiger qu'un
compilateur calcule de mani=E8re correcte sur les entiers (1+1 doit
toujours =EAtre =E9gal =E0 2), ce qui est en soi une garantie
(ext=E9rieure) de l'identit=E9 des r=E9sultats. Mais lorsque le calcul a
lieu sur des nombres d=E9cimaux, le r=E9sultat est souvent arrondi. Il
n'y a pas alors de "r=E9sultat juste" (ex. 1/3 n'a pas d'expression
d=E9cimale finie juste, m=EAme si certaines sont meilleures que
d'autres). Comment sait-on que la mani=E8re de calculer et d'arrondir
sera la m=EAme sur chaque compilateur ?

Merci pour vos =E9claircissements.
Jul.

10 réponses

1 2 3 4 5
Avatar
kanze
Jean-Marc Bourguet wrote:

float et double sont classiquement des nombres en virgule flottante
binaires.


Juste un détail, mais tu n'as pas dû lire les classiques. Sur
l'IBM 1401, ils étaient base 10 (et pour être classique, c'est
une classique), et sur l'architecture la plus « classique »
encore répandue aujourd'hui, ils sont base 16. Ce n'est que sur
les ordinateurs ultra-moderne et avant garde, dont la conception
de l'architecture rémonte à moins de quarante ans, où on peut
être sûr de trouver le binaire.

--
James Kanze GABI Software
Conseils en informatique orientée objet/
Beratung in objektorientierter Datenverarbeitung
9 place Sémard, 78210 St.-Cyr-l'École, France, +33 (0)1 30 23 00 34

Avatar
Jean-Marc Bourguet
Sylvain writes:

la question n'est dès lors plus "entre différents compilateurs" mais
entre différents CPU. bcp, surement pas tous, utilisent les normes
IEEE déjà cités,


Ils utilisent le format d'IEEE-754, mais prennent plus de libertes
quant au comportement demande. Ces libertes concernent surtout les
denormaux mais ca ne m'etonnerait pas que la precision sur une racine
carree par exemple ne soit pas celle que la norme specifie.

donc ils sont censés se comporter à peu près pareil ... pour autant
l'identité n'est pas garantie et _surtout_ ne doit pas être cherchée
(un code numérique ne contient jamais d'opérateur == ...ok sauf pour
les boucles)


C'est une maniere de simplifier le probleme.

[...] Il facile d'exiger qu'un compilateur calcule de manière correcte
sur les entiers [...]. Mais lorsque le calcul a
lieu sur des nombres décimaux, le résultat est souvent arrondi.


toujours même sauf cas particuliers (0, NaN, inf).


Il y a un peu plus de cas particuliers que cela.

A+

--
Jean-Marc
FAQ de fclc++: http://www.cmla.ens-cachan.fr/~dosreis/C++/FAQ
C++ FAQ Lite en VF: http://www.ifrance.com/jlecomte/c++/c++-faq-lite/index.html
Site de usenet-fr: http://www.usenet-fr.news.eu.org


Avatar
Jean-Marc Bourguet
"kanze" writes:

Jean-Marc Bourguet wrote:

float et double sont classiquement des nombres en virgule flottante
binaires.


Juste un détail, mais tu n'as pas dû lire les classiques. Sur
l'IBM 1401, ils étaient base 10 (et pour être classique, c'est
une classique), et sur l'architecture la plus « classique »
encore répandue aujourd'hui, ils sont base 16. Ce n'est que sur
les ordinateurs ultra-moderne et avant garde, dont la conception
de l'architecture rémonte à moins de quarante ans, où on peut
être sûr de trouver le binaire.


J'ai dit float et double, en parlant des types C++. Tu as vu une
implementation de C++ ou float et double n'etaient pas binaires?


Quant a l'histoire, si j'ai bien compris, a cette epoque les machines
etaient decimales ou binaires; celles destinees aux charges
"commerciales" -- remplacement des tabulatrices -- decimales, celles
destinees aux charges "scientifiques" -- calculs -- binaires. Donc
j'ai l'impression que les flottants dans leur domaine d'utilisation
naturel sont classiquement binaires. Ce qui ne veut pas dire qu'il
n'y a pas eu une machine ou deux decimales destinnees au calcul
scientifique, ni qu'il n'y a pas eu de machines commerciales binaires,
ni qu'on a jamais fait de calcul sur des machines commerciales.

(Question implementation, d'apres ce que j'ai lu, il y avait souvent
une base binaire soujacente au comportement architectural decimal).

A+

--
Jean-Marc
FAQ de fclc++: http://www.cmla.ens-cachan.fr/~dosreis/C++/FAQ
C++ FAQ Lite en VF: http://www.ifrance.com/jlecomte/c++/c++-faq-lite/index.html
Site de usenet-fr: http://www.usenet-fr.news.eu.org


Avatar
kanze
Sylvain wrote:
jul wrote on 15/05/2006 14:28:


[...]
Ma question est donc la suivante : la norme impose-t-elle
l'identité des résultats de calculs entre différents
compilateurs ? Si oui, au moyen de quel(s) critère(s) ?


la question n'est dès lors plus "entre différents compilateurs" mais
entre différents CPU.


Pas tout à fait (ou plutôt, la question est beaucoup plus
complexe que ça).

bcp, surement pas tous, utilisent les normes IEEE déjà cités,
donc ils sont censés se comporter à peu près pareil ...


La norme IEEE concerne surtout la représentation en mémoire.

Ce qui est important ici, c'est que la norme C++ n'impose pas
une précision fixe au compilateur. Surtout, il permet que les
calculs et les résultats intermédiaires aient une précision plus
grand que celle du type -- sur un processeur Intel, il est
courant que tous les calculs et les résultats intermédiaire dans
les régistres sont systèmatiquement des long double (avec sur
Intel, une précision de 64 bits, plutôt que les 52 d'un double).

Le résultat, c'est que les résultats diffèrent selon que le
compilateur a réussi à garder les résultats intermédiaires en
régistre ou non, ou selon qu'il a trouvé une sous-expression
déjà calculée qu'il réutilise. C-à-d que tu peux avoir deux
résultats différents avec le même compilateur, sur le même CPU,
selon le niveau d'optimisation, et pour une sous expression, où
se trouve la sous-expression dans l'expression complète.

pour autant l'identité n'est pas garantie


Sauf quand c'est garantie. Il faut savoir ce qu'on fait.

et _surtout_ ne doit pas être cherchée (un code numérique ne
contient jamais d'opérateur == ...ok sauf pour les boucles)


Ça dépend de ce qu'on fait.

[...] Il facile d'exiger qu'un compilateur calcule de manière
correcte sur les entiers [...]. Mais lorsque le calcul a
lieu sur des nombres décimaux, le résultat est souvent arrondi.


toujours même sauf cas particuliers (0, NaN, inf).


Sur les nombres décimaux, qui sait. Disons que dans la
comptabilité, il y a des règles qu'il faut suivre.

Mais je crois que par nombres décimaux, le posteur original
voulait dire plutôt les nombres flottants de la machine. La,
parfois, les arrondis dépendent de la niveau d'optimisation du
compilateur, et où l'expression se trouve dans une expression
plus grande ou dans le code. Considérons pour un instant le
programme assez simple :

#include <iostream>
#include <ostream>

int
main()
{
double i = 1.0, j = 3.0 ;
double test = i / j ;
double t1 = test * 3.1, t2 = i / j * 3.1 ;
if ( i / j > 1.0 / 3.0 ) {
std::cout << "x" << std::endl ;
} else {
std::cout << "y" << std::endl ;
}
return 0 ;
}

Sur ma machine Linux, compilé avec « g++ doubleprec.cc », il
affiche "x" ; compilé avec « g++ -O3 doubleprec.cc », il affiche
"y".

Voilà pour le « toujours même ». (Et tu rémarqueras que j'ai
pris soin d'éviter des comparaisons d'égalité.)

Comment sait-on que la manière de calculer et d'arrondir
sera la même sur chaque compilateur ?


pour un compilo et/ou un hard utilisant la même représentation
(c'est assez facilement atteint) et le même mode opératoire de
calcul (le même cablage) (ça c'est jamais le cas - chaque
fondeur choisit ses propres tables) la manière de calculer et
d'arrondir est identique; mais il existe des différences
(subtiles) dans le hard qui influenceront les arrondis et le
résultat final.


Si on se limite à IEEE...

Il existe des règles d'arrondi, que la plupart, sinon tous les
hardware respectent. Dans certains cas:-). Certains compilateurs
ont des options pour imposer une variante très stricte, ou
chaque résultat intermédiare est forcé à la précision d'un
double (ni plus, ni moins). Du coup, les résultats sont bien
réproduceables. Mais le calcul est beaucoup plus lent.

si votre question était donc simplement: peut-on garantir des
identités, la réponse est non, non pour un même programme sur
le même CPU, non pour le même programme sur 2 CPU/compilateurs
différents.

si votre question est: peut-on obtenir les "mêmes résultats"
entre différents compilateurs et/ou CPU, la réponse est oui
parce qu'on ne doit pas parler "d'identité" mais simplement de
résultat connu à une précision donnée.


Le problème, c'est que c'est faux, même sur un processeur donné,
avec un compilateur donné.

la difficulté d'un code numérique n'est jamais de chercher une
identité


Attention : c'est vrai dans beaucoup de cas, surtout quand on ne
considère que ce qu'on appelle habituellement le numérique. Mais
dans des conditions bien contrôlées, si on sait ce qu'on fait,
c'est possible à utiliser les comparaisons pour équalité aussi.
Et les éviter n'est pas une garantie non plus -- voir mon
exemple ci-dessus.

(cela ne doit jamais être fait, bis), mais de coder tout un cycle de
calcul de manière à conserver le maximum de précision


Ce qui exige déjà une certaine compréhension des choses.

Il y a eu une discussion ici il y a un certain temps, a propos
de faire une somme des valeurs dans un tableau. J'ai
collectionné les solutions proposée ; on les trouverait (dans le
forme d'un benchmark, mais j'y ai ajouté des tests de précision)
à http://kanze.james.neuf.fr/code/Benchmarks/FloatingPointSum/.
(Note que c'est quelque chose que j'ai fait plutôt pour
moi-même. La présentation n'est donc pas terrible.) C'est
intéressant à noter les différences selon l'algorithme (sur un
Sun Sparc -- sur un Intel, je n'en constate pas avec le jeu de
données par défaut, probablement parce que le compilateur
réussit à garder tout en régistre, avec la précision
supplémentaire). Et que l'algorithme le plus rapide (et le plus
simple et le plus intuitif), c'est celui qui donne toujours le
plus d'erreurs.

--
James Kanze GABI Software
Conseils en informatique orientée objet/
Beratung in objektorientierter Datenverarbeitung
9 place Sémard, 78210 St.-Cyr-l'École, France, +33 (0)1 30 23 00 34


Avatar
Fabien LE LEZ
On 16 May 2006 01:46:08 -0700, "kanze" :

Il existe (je crois) des spécialistes qui savent effectuer des
calculs avec ces types, mais AMHA ils sont rares -- et a
priori, nous n'en faisons pas partie.


Pour quels nous ?


L'OP et moi.


Avatar
Sylvain
kanze wrote on 16/05/2006 12:04:
Sylvain wrote:
jul wrote on 15/05/2006 14:28:


[...]
Ma question est donc la suivante : la norme impose-t-elle
l'identité des résultats de calculs entre différents
compilateurs ? Si oui, au moyen de quel(s) critère(s) ?


la question n'est dès lors plus "entre différents compilateurs" mais
entre différents CPU.


Pas tout à fait (ou plutôt, la question est beaucoup plus
complexe que ça).


ok, j'ai un peu simplifié, j'aurais du dire: la question est de savoir
si l'identité peut être garantie au niveau du CPU avant de se demander
si elle a un sens au niveau du compilo (et je serais arrivé à la même
conclusion).

bcp, surement pas tous, utilisent les normes IEEE déjà cités,
donc ils sont censés se comporter à peu près pareil ...


La norme IEEE concerne surtout la représentation en mémoire.


oui.

Ce qui est important ici, c'est que la norme C++ n'impose pas
une précision fixe au compilateur. Surtout, il permet que les
calculs et les résultats intermédiaires aient une précision plus
grand que celle du type -- sur un processeur Intel, il est
courant que tous les calculs et les résultats intermédiaire dans
les régistres sont systèmatiquement des long double (avec sur
Intel, une précision de 64 bits, plutôt que les 52 d'un double).


je note ce point - que tu as raison de rappeller - il est important pour
la suite.

Le résultat, c'est que les résultats diffèrent selon que le
compilateur a réussi à garder les résultats intermédiaires en
régistre ou non, ou selon qu'il a trouvé une sous-expression
déjà calculée qu'il réutilise.


soit les compilos ne génèrent pas tous le même code, mais le premier
facteur perturbant reste le développeur qui stockera ou non des
résultats intermédiaires (à tort, exemple 1) ou organisera son code
différemment.

ex.1: (toutes variables de type float)
float a = (b + c/d) / (b - c/d);
est plus précis que
float t = c/d;
float a = (b + t) / (b - t);

pour autant l'identité n'est pas garantie


Sauf quand c'est garantie. Il faut savoir ce qu'on fait.


?? pour la première partie, on l'a déjà indiqué, non ? 0.0 == 0.0 est
garanti, nous sommes d'accord.

pour le reste, que je note également pour la suite, oui, il est utile de
savoir ce que l'on fait.

et _surtout_ ne doit pas être cherchée (un code numérique ne
contient jamais d'opérateur == ...ok sauf pour les boucles)


Ça dépend de ce qu'on fait.


je ne sais pas ce que tu veux dire.
si "ce qu'on fait" est un code non déterministe, c'est bien.

[...] Il facile d'exiger qu'un compilateur calcule de manière
correcte sur les entiers [...]. Mais lorsque le calcul a
lieu sur des nombres décimaux, le résultat est souvent arrondi.


toujours même sauf cas particuliers (0, NaN, inf).


Sur les nombres décimaux, qui sait. Disons que dans la
comptabilité, il y a des règles qu'il faut suivre.


la compta fait 99.99% de ses traitements en long ou long long (type comp
du pascal des années 70); je ne pense pas que ce domaine non enseigne
grand chose.

Mais je crois que par nombres décimaux, le posteur original
voulait dire plutôt les nombres flottants de la machine.


je pense aussi, d'autres posts avant moi avait noté cette imprécision.

parfois, les arrondis dépendent de la niveau d'optimisation du
compilateur, et où l'expression se trouve dans une expression
plus grande ou dans le code. Considérons pour un instant le
programme assez simple :

int main(){
double i = 1.0, j = 3.0 ;
if ( i / j > 1.0 / 3.0 ) {
std::cout << "x" << std::endl ;
} else {
std::cout << "y" << std::endl ;
}
return 0 ;
}

Sur ma machine Linux, compilé avec « g++ doubleprec.cc », il
affiche "x" ; compilé avec « g++ -O3 doubleprec.cc », il affiche
"y".

Voilà pour le « toujours même ». (Et tu rémarqueras que j'ai
pris soin d'éviter des comparaisons d'égalité.)


je suis pas sur de comprendre l'esprit des remarques, donc je détaille:

tu cites mon "toujours même" comme s'il était inexact.
- le contexte était:
cotation: "le résultat est souvent arrondi"
réponse: "toujours même"
i.e. "un résultat est en effet toujours arrondi" - on peut même dire que
0.0 est arrondi à 0.0

si ton point était plutôt: "non ce n'est pas toujours le même arrondi":
je n'ai pas écrit le contraire, mais ton illustration passe par 2 codes
différents (via des options de compil différentes), il est dans ce cas
simplement non surprenant du tout de ne pas obtenir le même résultat.

finalement, tu indiques ne pas utiliser d'égalité (en réponse j'imagine
à mon conseil de ne jamais utiliser d'opérateur ==); mais quelle
différence fais-tu un test strict d'équalité et un test strict
d'ordonnencement ? coder if (float1 > float2) est aussi maladroit que
d'écrire "==" ou ">=" ou n'importe quel opérateur immédiat de
comparaison (« on ne peut que tester des écarts par rapport à une erreur
relative »).

Comment sait-on que la manière de calculer et d'arrondir
sera la même sur chaque compilateur ?
pour un compilo et/ou un hard utilisant la même représentation

[...]
Si on se limite à IEEE...



j'avais indiqué avant que c'était le cas pour la plupart des CPU
courants et des compilos, donc je m'autorisais cette limitation.

Il existe des règles d'arrondi, que la plupart, sinon tous les
hardware respectent. Dans certains cas:-). Certains compilateurs
ont des options pour imposer une variante très stricte, ou
chaque résultat intermédiare est forcé à la précision d'un
double (ni plus, ni moins). Du coup, les résultats sont bien
réproduceables. Mais le calcul est beaucoup plus lent.


un peu plus lent ... on trouvera des différences vraiment significatives
que pour des architectures assez spécifiques.

si votre question est: peut-on obtenir les "mêmes résultats"
entre différents compilateurs et/ou CPU, la réponse est oui
parce qu'on ne doit pas parler "d'identité" mais simplement de
résultat connu à une précision donnée.


Le problème, c'est que c'est faux, même sur un processeur donné,
avec un compilateur donné.


non ce n'est pas faux - relis ma phrase: "un résultat est connu à une
précision donnée", si pour toi il est faux que: 1.0 + 1.0 = 2.0 +/- 2.0
ou encore 1.0 + 1.0 = toute valeur entre 0.0 et 4.0 si je considère que
mon résultat est juste à 2.0 près, ... c'est qu'on a un pb de langage.

je n'ai jamais dit que l'on obtenait "le même résultat à 10^-32 près sur
tous les CPU, compilos, etc".

(cela ne doit jamais être fait, bis), mais de coder tout un cycle de
calcul de manière à conserver le maximum de précision


Ce qui exige déjà une certaine compréhension des choses.

Il y a eu une discussion ici il y a un certain temps, a propos
de faire une somme des valeurs dans un tableau. J'ai
collectionné les solutions proposée ; on les trouverait (dans le
forme d'un benchmark, mais j'y ai ajouté des tests de précision)
à http://kanze.james.neuf.fr/code/Benchmarks/FloatingPointSum/.
(Note que c'est quelque chose que j'ai fait plutôt pour
moi-même. La présentation n'est donc pas terrible.) C'est
intéressant à noter les différences selon l'algorithme (sur un
Sun Sparc -- sur un Intel, je n'en constate pas avec le jeu de
données par défaut, probablement parce que le compilateur
réussit à garder tout en régistre, avec la précision
supplémentaire). Et que l'algorithme le plus rapide (et le plus
simple et le plus intuitif), c'est celui qui donne toujours le
plus d'erreurs.


tes routines sont intéressantes ! de mon point de vue, elles sont toutes
inadaptées et maladroites (ce n'est pas une provocation!).

pour introduire mon point, je reprendrais un exemple lu des dizaines de
fois dans ce ng:

int sum = 0;
for (int i = 0; i < size; ++i)
sum += nb[i];

où nb est un tableau de _int_

ce simple code est pour moi un bon exemple du cas où on ne sait pas ce
que l'on fait car stocker INT_MAX * INT_MAX dans un int est
nécessairement une erreur.

dans tes benchs tu accumules 1.000.000 de valeurs flottantes de type
float, or un float à une précision de 10^-6, en sommer 10^6 va
obligeatoirement faire perdre toute précision de l'élément ajouté par
rapport à l'accu courant - ie va générer un débordement de précision et
revient à faire un inf + epsilon en espérant trouver l'epsilon dans le
nouveau résultat.

l'autre point corrollaire pertinent est la précision intermédiaire que
tu évoquais; puisque les calculs sont en boucle le résultat
intermédiaire (qui contient qlq chose comme 500000.512345 en fin de
boucle) est tronqué à un float (6 chiffres significatifs, donc 500000.)
et donc fausse complètement le résultat.

(btw, "complétement" est à relativiser, les sommes sont différentes (ou
égales) à 10^-5 près, c'est homogène à ce que l'on peut attendre de
calculs fait en float).

la routine sumCompensated est "amusante" car elle semble user de
compléxité et d'ingéniosité pour simplement récupérer la précision que
l'on fait exprès de détruire.

or comme une somme de char doit être stockée dans un short, des shorts
dans un long et des longs dans un long long, il suffit simplement des
sommer le tableau de float 'table' dans un double pour conserver la
précision voulue (500000.0 + 0.512345 = 500000.512345), ainsi:

float s1 = sumUp<float>(table, arraySize);
float s2 = sumDown<float>(table, arraySize);
float s3 = sumBinary<float>(table, arraySize);
float s4 = sumCompensated<float>(table, arraySize);

donnent 4 résultats différents (s3, s4 tendent à être identiques au prix
d'une récursivité dont on devrait avant tout enseigner le cout si ce
n'est le danger), alors que:

double d1 = sumUp<double>(table, arraySize);
double d2 = sumDown<double>(table, arraySize);
double d3 = sumBinary<double>(table, arraySize);
double d4 = sumCompensated<double>(table, arraySize);

donnent 4 fois exactement le même résultat.

comme tu disais,il est important de savoir ce que l'on fait -- et ici il
faut comprendre que garder 6 chiffres significatifs entre 2 opérandes
ayant 6 magnitudes d'écart impose de traiter ces opérations avec 12
chiffres significatifs.

il vient que (array étant un float[]):

double result = 0.0;
for (size_t i = 0; i < size; ++i)
result += array[i];

est bien l'écriture la plus compacte, simple et intuitive qui donne le
bon résultat.

l'autre leçon est aussi encore et toujours liée au débordements
(relatifs entre opérandes), d'après mon point précédent il serait
impossible de sommer un milliard de float avec des double; en fait ce
sera possible si on se ramène à des opérandes de même magnitude, en
calculant par exemple 1000 sommes intermédiaires (en format double) sur
des paquets d'un million de valeurs, puis que l'on somme ces 1000
valeurs (homogènes) entre elles.

par exemple (pour "size" carré d'une puissance de 10, ce qui est le cas
d'un million):

template<typename T> T sumPart(float const* array, size_t size)
{
size = (size_t) sqrt(size);

T result = 0.0;
float const* t = array;
for (size_t k = 0; k < size; ++k){
T inter = 0.0;
for (size_t i = 0; i < size; ++i, ++t)
inter += *t;
result += inter;
}
return result;
}

donne également le bon résultat pour un type T float.

Sylvain.



Avatar
jul
Je programme une application qui fait un fort usage des
calculs avec des valeurs décimales (à virgule) avec les types
float ou double.


Tu crois ? Je ne connais pas de machine moderne où les float ou
les double sont des types décimaux. Binaire, la plupart du
temps, ou héxadécimal, mais pas décimaux


Deux précisions
1) J'ai manqué de clarté : j'entendais "décimaux" au sens
mathématique (c'est pourquoi j'ai mis "à virgule", bien que ce ne
soit pas si clair que ça). Je n'accorde aucune importance à la
représentation interne du nombre.
2) C'est moins le problème d'identité dans le code (utiliser ==
après deux calculs identiques) que celui de garantir la même sortie
entre deux exécutables compilés à partir du même code source,
sachant qu'une faible différence dans le résultat d'un calcul peut
entraîner de grosses différences dans ladite sortie.

Jul.


Avatar
kanze
Sylvain wrote:
kanze wrote on 16/05/2006 12:04:
Sylvain wrote:
jul wrote on 15/05/2006 14:28:


[...]
Ma question est donc la suivante : la norme impose-t-elle
l'identité des résultats de calculs entre différents
compilateurs ? Si oui, au moyen de quel(s) critère(s) ?


la question n'est dès lors plus "entre différents
compilateurs" mais entre différents CPU.


Pas tout à fait (ou plutôt, la question est beaucoup plus
complexe que ça).


ok, j'ai un peu simplifié, j'aurais du dire: la question est
de savoir si l'identité peut être garantie au niveau du CPU
avant de se demander si elle a un sens au niveau du compilo
(et je serais arrivé à la même conclusion).


Le problème ici, c'est que même au niveau du CPU, l'identité
entre les valeurs telles qu'on les stocke en mémoire, et telles
qu'elles sont calculées et stockées dans les régistres, n'est
pas garantie.

Un problème plus général, à mon avis, c'est que la
simplification dans ce domaine mène très souvent en erreur.
J'entends des choses comme il faut éviter d'utiliser == et !=,
ou qu'il faut toujours tester l'égalité à un epsilon près, comme
si on proposait une solution simple, qui résoud tous les
problèmes. Or, si tu sais bien ce que tu fais, tu peux très bien
utiliser == et != dans certains cas. Et si tu ne sais pas, tu
vas te planter même sans se servir de == ou de !=. Nous sommes
bien d'accord qu'il y a beaucoup de cas où == ou != marche
parfaitement bien avec des entiers, mais sont à éviter avec les
flottants, mais les éviter n'a pas l'effet d'un baton magic, qui
va rendre le code correct.

[...]
Ce qui est important ici, c'est que la norme C++ n'impose
pas une précision fixe au compilateur. Surtout, il permet
que les calculs et les résultats intermédiaires aient une
précision plus grand que celle du type -- sur un processeur
Intel, il est courant que tous les calculs et les résultats
intermédiaire dans les régistres sont systèmatiquement des
long double (avec sur Intel, une précision de 64 bits,
plutôt que les 52 d'un double).


je note ce point - que tu as raison de rappeller - il est
important pour la suite.


On peut le dire, vue qu'il fait que j'ai des résultats
différents selon le niveau d'optimisation, sans changer ni le
compilateur ni le CPU. (Et sans avoir un comportement indéfini
ni non-spécifié.)

Le résultat, c'est que les résultats diffèrent selon que le
compilateur a réussi à garder les résultats intermédiaires
en régistre ou non, ou selon qu'il a trouvé une
sous-expression déjà calculée qu'il réutilise.


soit les compilos ne génèrent pas tous le même code,


Par définition, l'activation de l'optimisation fait que le
compilateur génère un code différent. Sinon, il n'y aura pas
d'intérêt. Et deux compilateurs différents ne génèreront prèsque
jamais un code identique.

mais le premier facteur perturbant reste le développeur qui
stockera ou non des résultats intermédiaires (à tort, exemple
1) ou organisera son code différemment.


Disons que si le développeur veut avoir absolument un résultat
identique, il faut qu'il stocke le résultat de chaque calcul
intermédiaire. À la place de :
double disc = b*b - 4*a*c ;
il faudra écrire quelque chose du genre :
double tmp1 = b*b ;
double tmp2 = a*c ;
tmp2 *= 4 ;
double disc = tmp1 - tmp2 ;
Autant écrire en assembleur.

ex.1: (toutes variables de type float)
float a = (b + c/d) / (b - c/d);
est plus précis que
float t = c/d;
float a = (b + t) / (b - t);


Plus précis, je ne sais pas (mais c'est fort probable). Mais le
C++ ne garantit pas que les résultats soient identiques. (Il ne
garantit pas non plus qu'ils soient différents. Si les variables
sont tous double -- ce qui doit être le cas le plus souvent --
les résultats sont différents sur mon PC Linux, mais identique
sur mon Sparc.)

pour autant l'identité n'est pas garantie


Sauf quand c'est garantie. Il faut savoir ce qu'on fait.


?? pour la première partie, on l'a déjà indiqué, non ? 0.0 ==
0.0 est garanti, nous sommes d'accord.


Il y a d'autres cas. Il existe (ou a existé) au moins une
bibliothèque pour faire de l'arithmétique décimale (pour la
comptabilité) qui travaillait avec les double, et garantissait
l'égalité. Avec les double IEEE, tant que les valeurs sont
des entiers inférieur à 4,5E15, et que tu ne fasses pas de
division, tu as une représentation exacte. Après une division,
tu peux tripoter pour avoir un résultat qui correspond aussi à
ce que tu aurais eu avec une division entière aussi. À l'époque
où il n'y avait pas encore de long long, et que les long étaient
prèsque toujours 32 bits, il n'était pas rare d'utiliser de
telles astuces dans le calcul financier -- tu maintenais les
valeurs en centîmes, et pour calculer la TVA, par exemple, tu
multipliais d'abord par 196 (résultat encore exact !), puis
divisais par 1000, et appliquais les règles d'arrondies voulues
pour rétomber sur un entier. (C'était en fait un peu plus
compliqué, du fait qu'après la division, tu pourrais tomber sur
quelque chose comme ,499999999... quand le résultat exact était
,5. Mais quand tu divises en entier par 1000, tu sais que la
granularité du résultat est ,001 -- beaucoup plus que n'importe
quelle erreur d'arrondie.)

pour le reste, que je note également pour la suite, oui, il
est utile de savoir ce que l'on fait.

et _surtout_ ne doit pas être cherchée (un code numérique
ne contient jamais d'opérateur == ...ok sauf pour les
boucles)


Ça dépend de ce qu'on fait.


je ne sais pas ce que tu veux dire. si "ce qu'on fait" est un
code non déterministe, c'est bien.


Il y a des cas où == va bien. Principalement en dehors des
boucles, d'ailleurs. L'arithmetique flottante, correctement
fait, est déterministe. Les règles qui la déterminent ne sont
pas celles de l'arithmétique sur les réeles, mais elles sont
parfaitement déterministes quand même.

[...] Il facile d'exiger qu'un compilateur calcule de manière
correcte sur les entiers [...]. Mais lorsque le calcul a
lieu sur des nombres décimaux, le résultat est souvent arrondi.


toujours même sauf cas particuliers (0, NaN, inf).


Sur les nombres décimaux, qui sait. Disons que dans la
comptabilité, il y a des règles qu'il faut suivre.


la compta fait 99.99% de ses traitements en long ou long long
(type comp du pascal des années 70); je ne pense pas que ce
domaine non enseigne grand chose.


La compta est le seul domaine que je connais où les nombres
décimaux servent. Et j'ai bien vu beaucoup de comptabilité qui
se fait avec les doubles -- souvent à tort, c'est vrai, mais
parfois, dans l'esprit que j'ai expliqué ci-dessus. Les long
long (un type entier à 64 bits) sont rélativement récents, et un
long à 32 bits ne suffit pas toujours pour des valeurs entiers
(quand on multiplie par 196 pour calculer la TVA, par exemple,
ou par 655957 pour convertir les Euros en francs).

Historiquement, évidemment, c'était du BCD qui dominait, grace à
Cobol. Et je ne connais pas de type comp in Pascal -- c'est ni
dans le Wirth, ni dans la norme. (Mais je n'ai jamais vu un
Pascal qui n'était pas bourré d'extensions.)

Mais je crois que par nombres décimaux, le posteur original
voulait dire plutôt les nombres flottants de la machine.


je pense aussi, d'autres posts avant moi avait noté cette
imprécision.


Mais on ne va pas la perpétrer nous deux. Quand toi, tu dis
décimal, je supposerai décimal, parce que je suis convaincu que
tu connais la différence.

parfois, les arrondis dépendent de la niveau d'optimisation
du compilateur, et où l'expression se trouve dans une
expression plus grande ou dans le code. Considérons pour un
instant le programme assez simple :

int main(){
double i = 1.0, j = 3.0 ;
if ( i / j > 1.0 / 3.0 ) {
std::cout << "x" << std::endl ;
} else {
std::cout << "y" << std::endl ;
}
return 0 ;
}

Sur ma machine Linux, compilé avec « g++ doubleprec.cc », il
affiche "x" ; compilé avec « g++ -O3 doubleprec.cc », il affiche
"y".

Voilà pour le « toujours même ». (Et tu rémarqueras que j'ai
pris soin d'éviter des comparaisons d'égalité.)


je suis pas sur de comprendre l'esprit des remarques,


C'était juste pour montrer que la situation est loins d'être
simple -- que même avec le même compilateur sur le même
processeurs, on peut avoir des résultats différents (ici, à
cause des différentes options de compilation). Et qu'éviter de
tester pour == ou != ne suffisait pas pour éliminer l'ambiguïté.

donc je détaille:

tu cites mon "toujours même" comme s'il était inexact.
- le contexte était:
cotation: "le résultat est souvent arrondi"
réponse: "toujours même"
i.e. "un résultat est en effet toujours arrondi" - on peut
même dire que 0.0 est arrondi à 0.0


OK. J'ai appliqué le « toujours même » plus largement.
(Strictement parlant, je crois que « le résultat est toujours
arrondi » est une simplification de la réalité. Mais dans ce
cas-ci, il s'agit d'une simplification qui facilite la
compréhension et n'induit pas en erreur. J'y adhère donc.)

si ton point était plutôt: "non ce n'est pas toujours le même
arrondi": je n'ai pas écrit le contraire, mais ton
illustration passe par 2 codes différents (via des options de
compil différentes), il est dans ce cas simplement non
surprenant du tout de ne pas obtenir le même résultat.


Tu trouves que ce n'est pas étonnant que je n'ai pas obtenu le
même résultat ? J'avais l'impression que tu disais que les
résultats étaient indentiques sur un même processeur. C'est bien
un contre-exemple. (Mais peut-être j'ai mal compris ce que tu
voulais dire.)

finalement, tu indiques ne pas utiliser d'égalité (en réponse
j'imagine à mon conseil de ne jamais utiliser d'opérateur ==);


Tout à fait. Je voulais juste démontrere qu'éviter les == n'a
pas d'effet magique.

mais quelle différence fais-tu un test strict d'équalité et un
test strict d'ordonnencement ? coder if (float1 > float2) est
aussi maladroit que d'écrire "==" ou ">=" ou n'importe quel
opérateur immédiat de comparaison (« on ne peut que tester des
écarts par rapport à une erreur relative »).


Alors, de quoi sers-tu pour comparer, si tu n'utilises ni les
tests d'équalité, ni les tests d'ordonnencement ?

Comment sait-on que la manière de calculer et d'arrondir
sera la même sur chaque compilateur ?
pour un compilo et/ou un hard utilisant la même représentation

[...]
Si on se limite à IEEE...



j'avais indiqué avant que c'était le cas pour la plupart des
CPU courants et des compilos, donc je m'autorisais cette
limitation.

Il existe des règles d'arrondi, que la plupart, sinon tous
les hardware respectent. Dans certains cas:-). Certains
compilateurs ont des options pour imposer une variante très
stricte, ou chaque résultat intermédiare est forcé à la
précision d'un double (ni plus, ni moins). Du coup, les
résultats sont bien réproduceables. Mais le calcul est
beaucoup plus lent.


un peu plus lent ... on trouvera des différences vraiment
significatives que pour des architectures assez spécifiques.


Comme l'Intel 32 bits.

Mais la distinction « un peu plus lent »/« beaucoup plus lent »
dépend plus du point de vue que d'autres choses. Dans mes
applications, même si l'addition flottant était cinq fois plus
lent, ça ne serait qu'« un peu plus lent » au niveau de
l'application. En revanche, il y a des applications de calcul
numérique ou une perte de 10% serait perçu comme « beaucoup plus
lent ».

si votre question est: peut-on obtenir les "mêmes
résultats" entre différents compilateurs et/ou CPU, la
réponse est oui parce qu'on ne doit pas parler "d'identité"
mais simplement de résultat connu à une précision donnée.


Le problème, c'est que c'est faux, même sur un processeur
donné, avec un compilateur donné.


non ce n'est pas faux - relis ma phrase: "un résultat est
connu à une précision donnée", si pour toi il est faux que:
1.0 + 1.0 = 2.0 +/- 2.0 ou encore 1.0 + 1.0 = toute valeur
entre 0.0 et 4.0 si je considère que mon résultat est juste à
2.0 près, ... c'est qu'on a un pb de langage.

je n'ai jamais dit que l'on obtenait "le même résultat à
10^-32 près sur tous les CPU, compilos, etc".


Bien, je supposais que tu disais quelque chose d'utile. Qu'un
résultat soit connu à une précision donnée n'a d'utilité que si
on connaît cette précision. Et comme j'ai montré, cette
précision varie selon le contexte.

(cela ne doit jamais être fait, bis), mais de coder tout un
cycle de calcul de manière à conserver le maximum de
précision


Ce qui exige déjà une certaine compréhension des choses.

Il y a eu une discussion ici il y a un certain temps, a
propos de faire une somme des valeurs dans un tableau. J'ai
collectionné les solutions proposée ; on les trouverait
(dans le forme d'un benchmark, mais j'y ai ajouté des tests
de précision) à
http://kanze.james.neuf.fr/code/Benchmarks/FloatingPointSum/.
(Note que c'est quelque chose que j'ai fait plutôt pour
moi-même. La présentation n'est donc pas terrible.) C'est
intéressant à noter les différences selon l'algorithme (sur
un Sun Sparc -- sur un Intel, je n'en constate pas avec le
jeu de données par défaut, probablement parce que le
compilateur réussit à garder tout en régistre, avec la
précision supplémentaire). Et que l'algorithme le plus
rapide (et le plus simple et le plus intuitif), c'est celui
qui donne toujours le plus d'erreurs.


tes routines sont intéressantes ! de mon point de vue, elles
sont toutes inadaptées et maladroites (ce n'est pas une
provocation!).


Pas de problème -- elle ne sont pas de moi. J'étais simplement
curieux aux différences réeles de précision et de vitesse. (De
prèsque j'ai compris d'après les discussions, le sumCompensated
est plus ou moins la solution standard parmi les spécialistes
dans la numérique. Mais c'est forte bien possible que j'ai mal
compris.) Quelle serait ta solution ? Je l'ajoutera au
benchmark. (Par curiousité, plutôt qu'autre chose. Je soupçonne
qu'il n'y a pas de solution magique, et que même une solution
qui se mesure mal dans mon benchmark pourrait convenir dans
certaines contextes.)

Je pourrais ajouter qu'ici on a un problème où la précision
supplémentaire des valeurs intermédiaire sur l'Intel a un
avantage. Sur le PC Linux, j'ai exactement les mêmes résultats
dans les tests de précision, quelque soit l'algorithme utilisé.

pour introduire mon point, je reprendrais un exemple lu des
dizaines de fois dans ce ng:

int sum = 0;
for (int i = 0; i < size; ++i)
sum += nb[i];

où nb est un tableau de _int_

ce simple code est pour moi un bon exemple du cas où on ne
sait pas ce que l'on fait car stocker INT_MAX * INT_MAX dans
un int est nécessairement une erreur.


On est bien d'accord là.

dans tes benchs tu accumules 1.000.000 de valeurs flottantes
de type float, or un float à une précision de 10^-6, en sommer
10^6 va obligeatoirement faire perdre toute précision de
l'élément ajouté par rapport à l'accu courant - ie va générer
un débordement de précision et revient à faire un inf +
epsilon en espérant trouver l'epsilon dans le nouveau
résultat.


Pas vraiement. D'abord, les valeurs flottantes sont, par défaut,
toutes dans l'intervalle 0-1,0. Alors, je sais que je n'aurais
pas de débordement de valeur. Deuxièmement, vouloir savoir le
moyen d'un grand nombre valeurs me semble un problème raisonable
-- une réponse du genre on ne sait pas le faire avec un
ordinateur ne me semble pas acceptable. Ensuite, c'est une
collection des algorithmes dont on a parlé. Bien avant de
l'écrire, il m'était connu que l'algorithme « naïf » pourrait
poser des problèmes. Le but du benchmark, c'était plutôt de
determiner empiriquement combien de problèmes, et quel était le
coût de leur résolution. (Autant que je me rappelle, Jean-Marc a
posté l'algorithme sumPriority comme un exemple d'un algorithme
« naïf » dans l'autre sens -- c'est la solution trivialement
évident pour réduire l'erreur au maximum, mais à quel coût !)

(Évidemment, dans n'importe quelle utilisation réele,
j'utiliserais double pour l'accumulateur de la somme, et non
float. Ici, le but était en partie de voir le comportement des
algorithmes par rapport aux pertes de précision -- et on voit
plus clairement quand les pertes de précision sont plus
grandes.)

l'autre point corrollaire pertinent est la précision
intermédiaire que tu évoquais; puisque les calculs sont en
boucle le résultat intermédiaire (qui contient qlq chose comme
500000.512345 en fin de boucle) est tronqué à un float (6
chiffres significatifs, donc 500000.) et donc fausse
complètement le résultat.


Il doit l'être, selon la norme C++. D'après les résultats, j'ai
l'impression qu'il ne l'est pas avec le compilateur g++ sur
PC/Linux.

(btw, "complétement" est à relativiser, les sommes sont
différentes (ou égales) à 10^-5 près, c'est homogène à ce que
l'on peut attendre de calculs fait en float).


J'ai des erreurs de 2*10^-5, mais effectivement. Si par exemple
les valeurs initiales viennent des mesures physiques, il y a des
chances qu'elles ne sont précises qu'à 10^-3 ou à 10^-4. Dans
tels cas, tous les résultats ont une précision acceptable. Sans
doute le mot « systèmatiquement » prèterait moins à confusion
que « complètement ».

la routine sumCompensated est "amusante" car elle semble user
de compléxité et d'ingéniosité pour simplement récupérer la
précision que l'on fait exprès de détruire.


Il paraît que c'est la version préconcisée par les numéristes.
Parce qu'en effet, elle donne un maximum de précision dans un
minimum de temps.

or comme une somme de char doit être stockée dans un short,
des shorts dans un long et des longs dans un long long, il
suffit simplement des sommer le tableau de float 'table' dans
un double pour conserver la précision voulue (500000.0 +
0.512345 = 500000.512345), ainsi:

float s1 = sumUp<float>(table, arraySize);
float s2 = sumDown<float>(table, arraySize);
float s3 = sumBinary<float>(table, arraySize);
float s4 = sumCompensated<float>(table, arraySize);

donnent 4 résultats différents (s3, s4 tendent à être
identiques au prix d'une récursivité dont on devrait avant
tout enseigner le cout si ce n'est le danger),


Coût et danger rélatifs. sumBinary est nettement plus rapide que
sumPriority (un ordre de magnitude, quand même), et
statistiquement, marche aussi bien. (Le problème, évidemment,
c'est que ce n'est que statistiquement. Il existe des cas
dégénéré où elle n'aurait pas plus de précision que sumUp.) Et
la profondeur de la récursion est bien limitée à ln(N), où N est
la taille de la table -- c'est en général acceptable (sinon, on
pourrait pas utiliser quick sort).

alors que:

double d1 = sumUp<double>(table, arraySize);
double d2 = sumDown<double>(table, arraySize);
double d3 = sumBinary<double>(table, arraySize);
double d4 = sumCompensated<double>(table, arraySize);

donnent 4 fois exactement le même résultat.


Je sais. J'ai utilisé float exprès, vue que le but était de
comparer le comportement des *algorithmes*, par rapport aux
arondi. J'ai fait en sort de maximiser ce que je voulais
mesurer.

comme tu disais,il est important de savoir ce que l'on fait --
et ici il faut comprendre que garder 6 chiffres significatifs
entre 2 opérandes ayant 6 magnitudes d'écart impose de traiter
ces opérations avec 12 chiffres significatifs.


(7 chiffres significatifs, en fait).

Sauf sumUp et sumDown, je crois que je n'ai jamais de cas où il
y a six magnitudes d'écart. L'idée derrière sumPriority et
sumBinary, en fait, c'est que les magnitudes soient toujours à
peu près identiques : sumPriority le garantit (à un prix
exhorbitant), et sumBinary joue sur la statistique -- c'est
d'ailleurs pourquoi dans mes tests de précision j'ai fait des
tests avec le tableau trié ; c'est le cas le plus défavorable
pour sumBinary (en ce qui concerne la précision).

il vient que (array étant un float[]):

double result = 0.0;
for (size_t i = 0; i < size; ++i)
result += array[i];

est bien l'écriture la plus compacte, simple et intuitive qui
donne le bon résultat.


Il s'avère qu'on ne peut pas garantir non plus qu'elle donne le
bon résultat. Il y a toujours des erreurs d'arrondi. Moins
qu'avec float, mais il y en a quand même.

Dans mes tests, j'utilisais les tableaux avec une taille d'une
million d'éléments. Des tableaux assez petits, en somme, mais
j'ai une vieille machine, pas très rapide, qui sert aussi à
d'autres choses, et je n'ai pas voulu la surcharger trop pendant
trop longtemps. J'ai donc modifié mes tests initiaux pour que
les effets de l'arrondi se fassent sentir sur des tableaux plus
petits. (Il faut dire surtout que j'ai assez peu de mémoire. Et
quand le test se met à pager, ça ralentit tout, beaucoup.)

l'autre leçon est aussi encore et toujours liée au
débordements (relatifs entre opérandes), d'après mon point
précédent il serait impossible de sommer un milliard de float
avec des double; en fait ce sera possible si on se ramène à
des opérandes de même magnitude, en calculant par exemple 1000
sommes intermédiaires (en format double) sur des paquets d'un
million de valeurs, puis que l'on somme ces 1000 valeurs
(homogènes) entre elles. par exemple (pour "size" carré d'une
puissance de 10, ce qui est le cas d'un million):


Et qu'est-ce que tu penses que sumBinary fait ?

template<typename T> T sumPart(float const* array, size_t size)
{
size = (size_t) sqrt(size);

T result = 0.0;
float const* t = array;
for (size_t k = 0; k < size; ++k){
T inter = 0.0;
for (size_t i = 0; i < size; ++i, ++t)
inter += *t;
result += inter;
}
return result;
}

donne également le bon résultat pour un type T float.


C'est la même principe que sumBinary. Sauf que sumBinary va
beaucoup plus loin. Elle souffre aussi du même défaut : il
existe des cas dégénérés où elle aurait une perte de précision
importante.

Quant au « bons résultats », mes tests pour précision donne (ton
code se trouve dans le colonne « partition ») :

Sun Sparc, Solaris, Sun CC :

Floating point tests:
up binary priority compensated
partition
Random arrangements:
5.000515e-01 5.000361e-01 5.000361e-01 5.000361e-01
5.000362e-01
5.000485e-01 5.000361e-01 5.000361e-01 5.000361e-01
5.000365e-01
5.000494e-01 5.000361e-01 5.000361e-01 5.000361e-01
5.000364e-01
5.000466e-01 5.000361e-01 5.000361e-01 5.000361e-01
5.000364e-01
5.000362e-01 5.000361e-01 5.000361e-01 5.000361e-01
5.000361e-01
Sorted ascending:
5.000423e-01 5.000361e-01 5.000361e-01 5.000361e-01
5.000361e-01
Sorted descending:
5.000378e-01 5.000361e-01 5.000361e-01 5.000361e-01
5.000359e-01

PC, Linux, g++ :

Floating point tests:
up binary priority compensated
partition
Random arrangements:
5.000361e-01 5.000361e-01 5.000361e-01 5.000361e-01
5.000362e-01
5.000361e-01 5.000361e-01 5.000361e-01 5.000361e-01
5.000364e-01
5.000361e-01 5.000361e-01 5.000361e-01 5.000361e-01
5.000364e-01
5.000361e-01 5.000361e-01 5.000361e-01 5.000361e-01
5.000362e-01
5.000361e-01 5.000361e-01 5.000361e-01 5.000361e-01
5.000361e-01
Sorted ascending:
5.000361e-01 5.000361e-01 5.000361e-01 5.000361e-01
5.000360e-01
Sorted descending:
5.000361e-01 5.000361e-01 5.000361e-01 5.000361e-01
5.000361e-01

Les résultats sur Sparc correspond à peu près à ce que je
m'attends -- on fait mieux que l'algorithme naïf, mais on est
encore derrière les solutions « correctes ». Les résultats sur
PC sont encore plus intéressants -- pourquoi moins de précision
qu'avec la solution naïve ? Ce que je soupçonne (mais je n'ai
pas encore vérifié), c'est que g++ garde la variable
d'accumulation dans un régistre (avec donc la précision
supplémentaire), mais seulement pour la boucle intérieur. (Je ne
crois pas que c'est légal, si c'est réelement ce qui se passe.
La norme dit bien « The values of the floating operands and the
results of floating epxressions may be represented in greater
precision and range than that required by the type », mais il y
a une note qui dit que « The cast and assignment operators must
still perform their specific conversions as described in 5.4,
5.2.9 and 5.17», et dans §5.17, on lit que « The result of an
assignment operation is the value stored in the left operand
after the assignment has taken place; »)

Enfin, comme on voit, c'est complexe, et c'est difficile de
faire des déclarations simples qui valent. En fin de compte, il
faut bien comprendre ce qu'on fait -- et se poser pas mal de
questions sur ce que fait le compilateur.

--
James Kanze GABI Software
Conseils en informatique orientée objet/
Beratung in objektorientierter Datenverarbeitung
9 place Sémard, 78210 St.-Cyr-l'École, France, +33 (0)1 30 23 00 34




Avatar
kanze
jul wrote:
Je programme une application qui fait un fort usage des
calculs avec des valeurs décimales (à virgule) avec les
types float ou double.


Tu crois ? Je ne connais pas de machine moderne où les float
ou les double sont des types décimaux. Binaire, la plupart du
temps, ou héxadécimal, mais pas décimaux


Deux précisions
1) J'ai manqué de clarté : j'entendais "décimaux" au sens
mathématique (c'est pourquoi j'ai mis "à virgule", bien que ce
ne soit pas si clair que ça). Je n'accorde aucune importance à
la représentation interne du nombre.


Et quel est sa sens mathématique ? Je ne l'ai vu servir que pour
parler de la base de représentation. En mathématique, j'ai
traité surtout des entiers, des rationnels et des réels -- en
informatique, ce sont des entiers (qui ne sont pas tout à fait
des entiers des mathématiciens, n'étant pas infini) et des
virgules flottants (qui sont en fait très différent des réels ou
des rationnels des mathématiciens).

2) C'est moins le problème d'identité dans le code (utiliser
== après deux calculs identiques) que celui de garantir la
même sortie entre deux exécutables compilés à partir du même
code source, sachant qu'une faible différence dans le résultat
d'un calcul peut entraîner de grosses différences dans ladite
sortie.


C'est en fait à peu près ce que j'avais compris. Et si tu as lu
mes réponses (ce qui exige un certain effort, je l'avoue), tu
auras compris que la réponse est « pas du tout ». Même avec la
même source, le même compilateur et le même processeur, j'ai
deux résultats différent, selon les options de compilation -- en
gros, tu risques d'avoir des résultats différents entre la
version debug et la version que tu livres.

--
James Kanze GABI Software
Conseils en informatique orientée objet/
Beratung in objektorientierter Datenverarbeitung
9 place Sémard, 78210 St.-Cyr-l'École, France, +33 (0)1 30 23 00 34



Avatar
kanze
Jean-Marc Bourguet wrote:
"kanze" writes:

Jean-Marc Bourguet wrote:

float et double sont classiquement des nombres en virgule
flottante binaires.


Juste un détail, mais tu n'as pas dû lire les classiques. Sur
l'IBM 1401, ils étaient base 10 (et pour être classique, c'est
une classique), et sur l'architecture la plus « classique »
encore répandue aujourd'hui, ils sont base 16. Ce n'est que sur
les ordinateurs ultra-moderne et avant garde, dont la conception
de l'architecture rémonte à moins de quarante ans, où on peut
être sûr de trouver le binaire.


J'ai dit float et double, en parlant des types C++. Tu as vu
une implementation de C++ ou float et double n'etaient pas
binaires?


Oui. La plus classique de tous (en ce qui concerne le processeur
en général -- c'est vrai qu'il a mis du temps à se mettre à
C/C++), l'IBM 390.

Quant a l'histoire, si j'ai bien compris, a cette epoque les
machines etaient decimales ou binaires; celles destinees aux
charges "commerciales" -- remplacement des tabulatrices --
decimales, celles destinees aux charges "scientifiques" --
calculs -- binaires. Donc j'ai l'impression que les flottants
dans leur domaine d'utilisation naturel sont classiquement
binaires.


Plus ou moins. IBM, au moins, a toujours été une exception, et
l'architecture 360 offrait à la fois l'arithmetique décimal
(BCD) pour les applications commerciales, et des virgules
flottants hexadécimaux pour les calculs numériques (et se
vendaient bien dans les deux domaines). IBM a fini, il y a
quelques années, à offrir *aussi* de l'arithmétique IEEE sur ses
mainframes, mais comme extension, supplémentaire, et moins
rapide que le format hexadécimal traditionnel. (Il y a trois ans
et démi, au moins, le format hexadécimal était encore le seul
qu'IBM savait mettre sur la ligne -- on a dû écrire des
fonctions spéciales pour le transcoder en IEEE pour le Java sur
nos clients.)

Ce qui ne veut pas dire qu'il n'y a pas eu une machine ou deux
decimales destinnees au calcul scientifique, ni qu'il n'y a
pas eu de machines commerciales binaires, ni qu'on a jamais
fait de calcul sur des machines commerciales.

(Question implementation, d'apres ce que j'ai lu, il y avait
souvent une base binaire soujacente au comportement
architectural decimal).


Au plus bas niveau... J'ai entendu parlé une fois d'une machine
expérimentale qui utilisait le base 3, et d'après ce que j'ai
entendu dire, une partie au moins des tout premiers 8087 était
implémenté en base 4 (mais ça ne paraissait pas à l'exterieur --
c'était pour reduire la place nécessaire pour le microcode, je
crois). Mais c'est vraiment les cas exceptionnels -- quand on
parle du décimal sur une machine, c'est en général du BCD, avec
quatre bits binaires.

--
James Kanze GABI Software
Conseils en informatique orientée objet/
Beratung in objektorientierter Datenverarbeitung
9 place Sémard, 78210 St.-Cyr-l'École, France, +33 (0)1 30 23 00 34



1 2 3 4 5