float et double sont classiquement des nombres en virgule flottante
binaires.
float et double sont classiquement des nombres en virgule flottante
binaires.
float et double sont classiquement des nombres en virgule flottante
binaires.
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,
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)
[...] 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).
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,
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)
[...] 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).
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,
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)
[...] 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).
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.
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.
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.
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.
bcp, surement pas tous, utilisent les normes IEEE déjà cités,
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)
[...] 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).
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 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.
la difficulté d'un code numérique n'est jamais de chercher une
identité
(cela ne doit jamais être fait, bis), mais de coder tout un cycle de
calcul de manière à conserver le maximum de précision
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.
bcp, surement pas tous, utilisent les normes IEEE déjà cités,
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)
[...] 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).
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 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.
la difficulté d'un code numérique n'est jamais de chercher une
identité
(cela ne doit jamais être fait, bis), mais de coder tout un cycle de
calcul de manière à conserver le maximum de précision
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.
bcp, surement pas tous, utilisent les normes IEEE déjà cités,
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)
[...] 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).
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 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.
la difficulté d'un code numérique n'est jamais de chercher une
identité
(cela ne doit jamais être fait, bis), mais de coder tout un cycle de
calcul de manière à conserver le maximum de précision
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 ?
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 ?
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 ?
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.
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.
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é.)
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...
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 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é.
(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.
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.
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.
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é.)
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...
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 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é.
(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.
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.
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.
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é.)
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...
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 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é.
(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.
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
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
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
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).
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.
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).
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.
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).
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.
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.
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.
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.
"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).
"kanze" <kanze@gabi-soft.fr> 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).
"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).