Problème d'arrondi flottant

a marqué ce sujet comme résolu.

Bonjour J’ai un programme et j’ai besoin d’être précis (mesure de poids etc…), j’utilise des doubles et évidemment après une simple addition le problème intervient (0.765 + 0.222 = 0.98600000001). Est-ce que il y a un moyen tout prêt avec Qt qui règle ce problème ? Sinon j’ai pensé créer une classe qui manipulerait des int, mais j’ai peur que en convertissant ça me fasse le même souci…

Quelqu’un peut il m’éclairer ?

Salut,

En effet c’est un problème inhérent aux nombres à virgule flottant (et à la norme IEEE-754) qui ne permet pas de représenter tous les nombres décimaux et implique des arrondis / pertes de précision lors des calculs. Tu peux lire ce tutoriel pour en apprendre plus sur le sujet.

Des approximations sont donc utilisées pour les nombres qui ne peuvent pas être représentés exactement dans ce format, ce qui crée un léger écart entre le nombre souhaité et le nombre stocké. Lorsque tu additionnes deux flottants, tu additionnes aussi ces écarts et donc t’éloignes un peu plus du résultat souhaité (même si l’exemple que tu donnes me semble faux, 0.765+0.222 devrait plutôt donner quelque chose ressemblant à 0.987, pas 0.986).

La solution dépend de ce que tu fais avec ce résultat.

  • S’il ne s’agit que d’un affichage, tu peux arrondir au nombre de décimales voulues et cacher cette imprécision.
  • S’il s’agit de comparer avec un nombre, il est de toute façon recommandé de ne pas tester l’égalité entre flottants mais plutôt de tester l’écart entre-eux (vérifier que la valeur absolue de leur différence est inférieure à un seuil précis par exemple).
  • Enfin s’il s’agit d’un réel problème de perte de précision (tu as besoin de connaître/stocker le résultat exact de l’opération), la solution que tu proposes qui consiste à manipuler des int fonctionnerait.

On appelle ce dernier cas les nombres à virgule fixe (parce que tu représenterais par exemple tes nombres par 765 et 222 en sachant qu’ils représentent ta valeur x 1000). S’agissant de calcul sur des entiers tu ne rencontreras pas de nombres non représentables et obtiendras donc 987 comme résultat de l’opération.

Ensuite ce n’est qu’une question d’affichage, tu ne devrais dans ce cas pas stocker le résultat sous forme de float, seulement le convertir vers ce type lorsque c’est nécessaire pour l’afficher par exemple : tu ne cumuleras alors pas d’erreurs de précisions et ne rencontreras pas le problème que tu exposes.

Bon effectivement l’exemple n’est pas forcément vrai, mais c’est pour l’idée.

J’ai effectivement besoin de précision.

Donc si je converti juste pour l’affichage, je n’aurai pas de ce problème d’arrondi flottant ? (dans mon cas QString en double, qstring toDouble)

Mais sinon il n’existe pas d’autres solutions ? J’ai vu qu’il y avait des bibliothèques tierces, ce n’est pas une meilleure solution que de devoir implémenter un nouveau système ?

Comment font les banques ?

Et je n’arrive pas à comprendre pourquoi ce "bug" n’a pas été corrigé ?

Donc si je converti juste pour l’affichage, je n’aurai pas de ce problème d’arrondi flottant ? (dans mon cas QString en double, qstring toDouble)

NRG

Il y aura un arrondi mais qui ne sera normalement pas visible.
Sinon tu peux aussi gérer l’affichage toi-même (convertir l’int vers une chaîne de caractères en plaçant la virgule où tu le souhaites) mais je ne pense pas que ce soit nécessaire.

Mais sinon il n’existe pas d’autres solutions ? J’ai vu qu’il y avait des bibliothèques tierces, ce n’est pas une meilleure solution que de devoir implémenter un nouveau système ?

NRG

Si, par exemple représenter les nombres sous forme de fractions (rationnels) et ainsi garder des résultats exacts, mais c’est coûteux (en temps comme en mémoire).

Comment font les banques ?

NRG

Elles procèdent en virgule fixe principalement je pense, suivant la précision dont elles ont besoin (en centimes ou unités encore plus petites).

Et je n’arrive pas à comprendre pourquoi ce "bug" n’a pas été corrigé ?

NRG

Parce que ça reste négligeable dans beaucoup de cas et que les nombres flottants n’ont pas pour but d’offrir une représentation exacte des nombres mais de manipuler efficacement des nombres qui s’apparentent à des réels.

Et je n’arrive pas à comprendre pourquoi ce "bug" n’a pas été corrigé ?

NRG

Ce n’est pas un bug à proprement parler, mais une limitation due à la spécification des nombres à virgule flottante. Il faut comprendre qu’il est littéralement impossible pour un ordinateur de représenter certains nombres, il ne s’agit pas de quelque chose que l’on peut corriger. Cela se passe au niveau du processeur, Qt n’a aucun impact là-dessus. Tu peux même ouvrir la console JavaScript de ton navigateur et y entrer 0.1+0.2, tu n’obtiendras sûrement pas 0.3.

Les nombres à virgule flottante n’ont pas vocation à être précis. Il peut même y avoir une différence de résultat en effectuant le même calcul sur différents processeurs. Seuls les nombres entiers garantissent une précision indiscutable.

C’est ce que j’ai commencé à faire, je renvoie une chaînes de caractères = 0.245, si je converti ça sera précis non ?

Je sais que ce n’est pas un bug, mais pourquoi on crée des machines qui ne sont pas précises ? Quel est le but si sa vocation n’est pas d’être précis ?

Si tu as un besoin de précision absolu. Alors, sache que c’est pas forcément possible tout simplement. pi + e ou sqrt(2) par exemple, ne peuvent pas être calculés numériquement exactement.

Je sais que ce n’est pas un bug, mais pourquoi on crée des machines qui ne sont pas précises ? Quel est le but si sa vocation n’est pas d’être précis ?

L’ordinateur ne stockent que des nombre binaires. Or, en binaire, le nombre 0.2 n’est pas représentable de manière exacte. En effet, si on essaye, c’est 2**-3 + 2**-4 + 2**-7 + 2**-8 + 2**-11 + 2**-12 + 2**-15 + ... donc en binaire: 0.001100110011001.

Alors on trouve des astuces comme l’IEEE-754 qui nous permet de faire plus de choses, mais fondamentalement, le problème c’est que très peu de nombre décimaux à virgules (en base 10 donc) sont stockable de manière fini dans un ordinateur.


Mais si tu n’utilises que des nombres rationnels (un nombre fini de chiffres après la virgule en base 10), alors tu peux utiliser une bibliothèque telle que GMP pour résoudre ton problème. Mais c’est incroyablement plus lent (après si tu ne fais pas vraiment beaucoup de calculs, ben tu t’en fiches).

Si tu n’as pas besoin de précision absolu. Juste que l’affichage soit pas moche, tu peux simplement arrondir au nombre de chiffres significatifs.

Si c’est un peu des deux, tu peux utiliser long double qui est parfois disponible, sinon, ça sera équivalent à un double. C’est toujours1 de l’IEEE-754, quand disponible, mais sur 80bits plutôt que 64bits.

Ce petit programme en C chez moi :

#include <stdio.h>
#include <float.h>
#include <stdlib.h>

int main(void) {
  long double a = 0.1L, b = 0.2L;
  printf("%.20Lf\n", a + b);
  printf("Sizeof long double: %zu\n", sizeof (long double));

  return EXIT_SUCCESS;
}

Produit :

0.30000000000000000001
Sizeof long double: 16

Chez moi, en C, mais ça existe forcément également en C++.

#include <stdio.h>
#include <float.h>
#include <stdlib.h>
#include <quadmath.h>

int main(void) {
  __float128 a_q = 0.1Q, b_q = 0.2Q;
  char buf[32 + sizeof(".e+999999")];
  quadmath_snprintf(buf, sizeof(buf), "%.35Qf", a_q + b_q);
  puts(buf);

  long double a_l = 0.1L, b_l = 0.2L;
  printf("%.20Lf\n", a_l + b_l);

  double a = 0.1, b = 0.2;
  printf("%.17f\n", a + b);

  float a_f = 0.1, b_f = 0.2;
  printf("%.8f\n", a_f + b_f);

  return EXIT_SUCCESS;
}

Produit:

0.30000000000000000000000000000000004
0.30000000000000000001
0.30000000000000004
0.30000001

PS: Il est possible d’utiliser un truc qui s’appelle les nombres à virgules fixes pour stocker 0.2, mais le concept même de virgule fixes est de fixer une certaine précision.


  1. C’est faux, ça peut être autre chose, mais en pratique je n’ai pas encore trouvé de système où ça ne l’était pas.
+0 -0

Bonjour,

Comment font les banques ?

Les banques ne travaillent jamais en virgule flottante, car elles ne peuvent se permettre aucune erreur d’arrondi.

En plus elles manipulent tantôt des grands nombres (des milliards), tantôt des petits nombres (des opérations de trading au millionième de centime).

Quand les ordres de grandeurs sont potentiellement si différents, ça renforce d’autant plus les imprécisions au fur et à mesure des calculs.

Je sais que ce n’est pas un bug, mais pourquoi on crée des machines qui ne sont pas précises ? Quel est le but si sa vocation n’est pas d’être précis ?

Comme déjà dit, ce n’est pas un bug, et le comportement est suffisant ou acceptable dans la plupart des cas. En général, la précision est largement suffisante pour ce qu’on veut représenter.

Il faut bien voir qu’il existe une infinité de nombres réels. Or en informatique, les nombres sont stockés dans une quantité fixe de bits, typiquement 32 ou 64 bits. Donc la plupart des nombres réels, fatalement, tu ne peux pas les écrire exactement. Il faut décider de ce que tu veux pouvoir représenter, et ce que tu peux accepter comme inexactitude.

IL existe bien sûr des bibliothèques qui permettent de faire du calcul rigoureusement exact. Elles utilisent un nombre variable adéquat de bits pour représenter les nombres, mais elles ne viennent pas sans inconvénient… notamment, elles sont beaucoup plus lentes.

Si tu veux toujours garder le maximum de précision, chaque opération fait potentiellement doubler le nombre de bits nécessaires pour représenter le résultat exactement (un nombre de n bits multiplié par un nombre de m bits occupera n+m bits). C’est très impactant à la longue.

Faisons une petite analogie avec le système décimal pour comprendre le compromis qui a été conçu dans la norme IEE-754, peut-être que tu comprendras mieux.

Admettons que tu n’est autorisé à écrire des nombres que sur 3 chiffres significatifs, sous la forme XXX * 10^Z. Dans le vocabulaire consacré, XXX est la mantisse, et Z l’exposant.

  • 012 + 034 = 056, pas de problème
  • 1,23 + 4,56 = 5,79, pas de problème non plus
  • 999 + 1 = 1000 = 100 * 10^1, on a le droit
  • 1 / 4 = 0,25 (`110^0 / 4100 = 25*10-2), pas de problème, ça fonctionne
  • 100 / 3 = 33,333333… qu’on va arrondir / couper à 333 * 10^-1. Par contre en voulant faire 3 * 33,3, on tombera sur 99,9 et pas 100, c’est le problème d’arrondi.
  • 999 + 2 = 1001. ON peut écrire 100 * 10^1, mais on a perdu le 1. C’est le phénomène d’absorption. Au-delà de 1000, on n’a seulement une précision de 10. ON a le même phénomène avec 100 + 0,5, ou avec 1 + 0,0001.
  • En prenant 200 / 3 arrondi à 66,7, c’est intéressant: remultipliés tels quels par 3, on arrive à 200,1, mais au final on retombe sur nos pieds à 200 car le 0,1 est au-delà des 3 chiffres significatifs (arrondi+absorption, là ça nous arrange mais en pratique c’est pas toujours aussi cool)

En informatique, on a les mêmes phénomènes qui se produisent, mais en binaire. Sauf erreur pour la norme IEE-754 on a 52 bits au lieu de 3 chiffres significatifs, mais sinon le fonctionnement est plus ou moins pareil !

Donc en gros, tu dois choisir entre des calculs rapides, avec une quantité de bits fixes, mais parfois imprécis, ou alors des calculs exacts, avec une représentation sur une quantité de bits variable, mais nettement plus lents.

ET on s’est aperçu qu’en fait, la précision absolue, on n’en avait la plupart du temps pas besoin. Au-delà d’une certaine limite, ça n’a plus tant d’importance. Dans les jeux on n’a pas besoin d’une précision au millième de millimètre. En sciences souvent non plus, car les appareils de mesure ont leurs limites. Souvent pas non plus dans l’industrie, car une machine plus précise est plus chère et du coup plus forcément rentable, et là aussi il y a des limites physiques. Il reste les maths théoriques, et la finance, entres autres…

+1 -0

C’est vrai que j’en ai jamais eu besoin jusqu’à maintenant, et comme je fais un programme qui gère entre autres de l’argent…

Du coup les banques utilisent une bibliothèque style gmp, ou plutôt comme dis plus haut 2 int ?

Le problème d’utiliser par exemple un float ceux n’est pas forcément compatible (sans conversion) avec la partie graphique, et il existe potentiellement un risque (infime) d’erreur.

Et du coup ultime question, si j’utilise un qstring que je converti en double pour l’affichage, est ce qu’il y aura une erreur ?

+0 -0

Je sais que ce n’est pas un bug, mais pourquoi on crée des machines qui ne sont pas précises ?

Alors créons des machines analogiques pour lesquelles le voltage représente la valeur.

Mais le voltage dépend du nombre d’électrons accumulés.

On ne peut pas couper des électrons en deux ou autres.

Même dans ce cas, on n’aurait pas de précision infinie.

Physique quantique oblige (voir la constante de Planck)

Avec 64 bit signés, on représente 100 millions de milliards d’euros

J’ai un peu moins dans mon compte de banque …

On fait généralement les calculs de précision en virgule fixe et on fait la conversion en virgule flottante seulement au moment de passer les valeurs aux fonctions d’affichage parce que c’est ce qu’elles attendent. Mais les fonctions qui affichent font une conversion inverse en nombre de pixels, donc en virgule fixe.

+0 -0

Du coup les banques utilisent une bibliothèque style gmp, ou plutôt comme dis plus haut 2 int ?

Style GMP oui. Voir carrément GMP.

Elles sont assurées de toujours manipuler des valeurs ayant un nombre fini de chiffres (en base 10), donc dans leur cas, GMP (qui fonctionne en utilisant deux grands nombres aa et bb et en travaillant sur ab\frac{a}{b}) garanti de toujours marcher.

+1 -0

Comme tu dis que tu manipules de l’argent, si tu n’as pas besoin de fraction de centimes, tu peux juste utiliser des centimes (donc 1€ = 100) et pour l’afficher sans erreur d’arrondi, tu sépares le nombre en deux:

QString("%1.%2").arg(solde / 100).arg(solde % 100);

Si tu as besoin de plus de chiffres après la virgule, tu peux créer ta propre classe template qui prend un nombre et son ordre de grandeur (si tu n’as pas besoin de dépasser les milliards de milliards). Par exemple :

Number ten = Number(10, 0);
Number tenth = Number(1, 1);
// on peut même imaginer un ordre de grandeur négatif pour les puissances de 10
Number hundred = Number(1, -2);

Ce qui permet (en redéfinissant les opérateurs) d’additionner des nombres avec des ordres de grandeurs différentes (e.g. ten + tenth == Number(101, 1) = 10.1) Et lors de l’affichage, tu remplaces le 100 de l’exemple précédent par 10^denom (si != 1).

Du coup les banques utilisent une bibliothèque style gmp, ou plutôt comme dis plus haut 2 int ?

NRG

Plus haut je ne parlais pas d’utiliser deux entiers mais un seul.

Deux entiers (un pour la partie entière et un pour la partie décimale) ce serait la galère :

  • déjà il faudrait pouvoir faire la différence entre 1.234 et 1.0234 donc tu ne pourrais pas juste stocker 234 en partie décimale mais plutôt inverser les chiffres par exemple, 432 et 43200.
  • mais alors ça rend toutes les opérations compliquées puisqu’il faut chaque fois considérer parties entières et décimales, réaligner les nombres et gérer correctement les retenues.

Donc non, dans le cas d’une banque par exemple si elle considère qu’elle a besoin d’une précision au dixième de centime d’euro, elle pourra stocker 1.234 sous la forme 1234 et faire tous les calculs sur cette base : la précision (virgule) est fixée (toujours 3 chiffres après la virgule).

@entwanne je suis parti sur 2 entier, j’ai vite remarqué le bourbier lol. j’en suis arrivé à la conclusion (sans gmp) il fallait utiliser qround à un moment. Même si étant donné que j’utilise plusieurs fois les multiplications la class garde son intérêt

@minirop j’ai fais exactement comme ça, avec un ordre de grandeur (pas encore fini de totalement tout implenter par contre)

Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte