Problèmes d'arrondi des nombres à virgule flottante

De RAD Studio
Aller à : navigation, rechercher

Remonter à Utilisation des routines des nombres à virgule flottante

Travailler avec des nombres à virgule flottante nécessite une certaine compréhension de la représentation interne des données. Les programmeurs doivent être conscients des problèmes de précision limitée :

  • Pour les valeurs intégrales (types entiers), vous devez considérer la possibilité de dépassement de la capacité.
  • Pour les valeurs à virgule flottante (précision simple, double ou étendue), vous devez considérer la possibilité d'une perte de précision.

Implications de la précision limitée

Plusieurs causes peuvent conduire à une perte de précision des nombres à virgule flottante :

  • Si vous assignez un littéral à virgule flottante (par exemple : 0.1) à une variable à virgule flottante, cette dernière peut ne pas avoir suffisamment de bits pour contenir la valeur désirée sans introduire d'erreur. Le littéral à virgule flottante peut nécessiter un très grand nombre, ou même un nombre infini de bits pour une représentation en précision infinie.
uses
  System.SysUtils;
var
  X: Single;
  Y: Double;
begin
  X := 0.1;
  Y := 0.1;
  Writeln('X =', X);
  Writeln('Y =', Y);
  Writeln(Format('Exponent X: %x', [X.Exp]));
  Writeln(Format('Mantissa Y: %x', [Y.Mantissa]));
  ReadLn;
end.
Sortie console :
X = 1.00000001490116E-0001
Y = 1.00000000000000E-0001
Exponent Y: 3FB
Mantissa Y: 1999999999999A
Dans le code ci-dessus, vous pouvez voir l'erreur dans le 9e chiffre de la représentation en simple précision. L'erreur apparaît également dans la représentation en double précision. Pour démontrer cette erreur, utilisons les valeurs brutes Exponent et Mantissa du nombre en double précision. La valeur hexadécimale $3FB de Exponent a la représentation décimale 1019. Etant donné que la représentation interne de nombres en double précision a un bias égal à 1023, Exponent = 1019 - 1023 = -4. La valeur hexadécimale $1999999999999A de Mantissa a la représentation binaire 11001100110011001100110011001100110011001100110011010. Par conséquent, la représentation binaire de Y est 1.1001100110011001100110011001100110011001100110011010*2-4 or 0.00011001100110011001100110011001100110011001100110011010*2-4. Notez que ce nombre est l'approximation en double précision. Le nombre exact 0.1 est représenté sous la forme de la fraction récurrente infinie 0.0(0011).
Toutefois, il est intéressant de noter que l'erreur maximale pouvant être produite de cette façon est 0.5 ulps (EN).
  • Si vous effectuez des opérations en virgule flottante, chaque étape (opération) peut alors introduire son erreur spécifique. Cela se produit car, dans certaines opérations, le résultat calculé ne peut pas être stocké avec une précision complète. Par exemple, si vous multipliez deux nombres, bits S1 avec bits S2 (cela est vrai pour les types intégraux et les types à virgule flottante), le résultat nécessite alors les bits S1 + S2 pour une précision complète.
La "quantité" d'erreur introduite par une opération dépend du modèle du processeur et du type de l'opération. Les opérations d'addition introduisent une erreur relativement faible. La multiplication introduit une erreur relativement élevée.

Il est important de comprendre que la perte de précision en virgule flottante (erreur) est propagée à travers les calculs, et c'est le rôle du programmeur de concevoir un algorithme qui est toutefois correct.

Une variable à virgule flottante peut être considérée comme une variable entière avec une échelle de puissance de deux. Si vous "forcez" la variable à virgule flottante en une valeur extrême, l'échelle sera automatiquement ajustée. C'est la raison pour laquelle vous avez l'impression qu'une variable à virgule flottante ne peut pas subir un dépassement de capacité. Et c'est effectivement vrai, mais d'autre part, il y a une autre menace : une variable à virgule flottante peut accumuler une erreur significative et/ou devenir dénormalisée.

Utilisation de plus grands types de données

Le moyen le plus facile de résoudre le problème de dépassement de capacité des entiers ou de perte de précision des nombres à virgule flottante (en général, effets de la précision limitée) consiste à utiliser des types de données de la même classe (intégrale ou à virgule flottante), mais à capacité augmentée. Ainsi, si un ShortInt est en dépassement de capacité, vous pouvez alors facilement basculer sur un LongInt, FixedInt ou Int64. De même, si un flottant en simple précision n'offre pas assez de précision, vous pouvez alors basculer sur un flottant en double précision. Mais vous devez tenir compte des points suivants :

  • Le type de données ayant une plus grande capacité de stockage peut toujours être insuffisant.
  • Le type de données ayant une plus grande capacité de stockage nécessite plus de mémoire, et probablement plus de cycles CPU dans les opérations.

Paramètres de contrôle

Sur la plate-forme 32 bits, le mot de contrôle FPU x87 (CW) a deux bits alloués pour la spécification du mode d'arrondi. Voir Intel® 64 and IA-32 Architectures Software Developer's Manual Volume 1: Basic Architecture > 8.1.5.3 Rounding Control Field (EN). Pour les programmes 64 bits, le registre de contrôle SSE, MSXCSR, spécifie le mode d'arrondi. Vous pouvez les modifier à l'aide de System.Math.SetRoundMode.

Certaines fonctions de la RTL qui opèrent avec des variables à virgule flottante peuvent être affectées par le mode d'arrondi FPU. La nature exacte des modifications dans les résultats des routines RTL basées sur le mot de contrôle FPU dépend des algorithmes implémentés. L'arrondi aura des effets sur chaque opération nécessitant un arrondi pour que le résultat tienne dans le type cible, par exemple la multiplication de nombres à virgule flottante impliquera presque toujours un arrondi. Si une fonction est composée de lots de multiplications à virgule flottante, elle sera fortement affectée par le mode d'arrondi.

Le mode d'arrondi est parfois utilisé pour implémenter une arithmétique d'intervalle : grosso modo, effectuer le même algorithme avec un mode d'arrondi vers le haut, le répéter avec un mode d'arrondi vers le bas, puis voir les différences entre les deux résultats. Cela vous donne une idée des erreurs potentielles introduites par l'arrondi et l'imprécision.

Cas d'utilisation

Calculs financiers

La virgule flottante IEEE n'est pas appropriée aux calculs financiers. La raison est que les exigences de précision sont généralement très strictes. Vous devriez considérer l'utilisation de types intégraux (Currency ou entiers primitifs) ou de types BCD.

L'unité Data.FmtBcd fournit le support des opérations BCD. Le format BCD a la fonctionnalité importante suivante : chaque chiffre décimal (chiffre en base 10) est codé avec 4 bits de mémoire (un demi octet).

Le code suivant montre comment utiliser une valeur TBcd en tant que variant, par souci de commodité :

Delphi :

var
  X: Variant;

begin
  X := VarFMTBcdCreate('0.1', 16 { Precision }, 2 { Scale });
  Writeln(VarToStr(X));

  // ...

C++ :

#include <Variants.hpp>
#include <FMTBcd.hpp>

int _tmain(int argc, _TCHAR* argv[]) {
  Variant x = VarFMTBcdCreate("0.1", 16 /* Precision */, 2 /* Scale */);
  printf("%ls", VarToStr(x).c_str());

 // ...

Sortie console :

0.1

Vous pouvez voir dans le code ci-dessus, qu'avec l'aide d'une variable BCD, la conversion du texte au format numérique est parfaite.

Pour les calculs financiers, vous pouvez utiliser le type Currency. Le type Currency est essentiellement un entier multiplié par 10000 (cette valeur permet une division exacte par 10). Vous pouvez stocker quatre chiffres décimaux dans une variable Currency, tout ce qui va au-delà de cette limite est arrondi.

Delphi :

var
  X, Y: Currency;

begin
  X := 0.1;
  Y := 0.00001;

  Writeln(X);
  Writeln(Y);

  // ...

C++ :

#include <System.hpp>

int _tmain(int argc, _TCHAR* argv[]) {
  Currency x, y;
  x = 0.1;
  y = 0.00001;

  printf("%2.14E\n", (double)x);
  printf("%2.14E\n", (double)y);

  // ...

Sortie console :

1.00000000000000E-0001
0.00000000000000E+0000

Vous pouvez voir l'implémentation C++ de Currency dans $BDS\include\rtl\syscurr.h.

L'intervalle de Currency est [-922337203685477.5807; 922337203685477.5807].

Calculs / Simulations physiques (scientifiques)

Les calculs scientifiques nécessitent en général des calculs considérables, et l'augmentation de la précision à virgule flottante n'est pas désirable. Les opérations en précision étendue sont moins supportées (voir Delphi pour x64).

Si vous utilisez SSE, n'oubliez pas qu'un registre SSE peut contenir deux variables en double précision ou quatre variables en simple précision. Vous pouvez ainsi effectuer en parallèle davantage d'opérations en simple précision que d'opérations en double précision.

Voici une approche très intéressante et très utile : utilisez la virgule flottante en simple précision, mais réduisez périodiquement l'erreur accumulée. La plupart des applications peuvent tolérer une petite perte de précision ; il est juste important d'annuler d'une manière ou d'une autre les déviations. Un exemple d'une telle implémentation est la rotation spatiale 3D en utilisant les quaternions. Voir Physically Based Modeling > Rigid Body Dynamics (ONLINE SIGGRAPH 2001 COURSE NOTES) (EN).

Traitement des signaux digitaux

Toutes les variables DSP sont en général "contaminées" par des erreurs. Vous devez considérer le compromis optimal entre la précision des données et la puissance de calcul.

Conclusions générales

"Ce que vous voyez n'est pas ce que vous obtenez"

Les nombres à virgule flottante écrits dans le code source avec des chiffres décimaux et les nombres à virgule flottante affichés sur l'écran diffèrent probablement de ce qui réside en mémoire. Ne supposez pas que ce que vous voyez sur la console représente exactement ce qui est en mémoire. La conversion de décimal en binaire (et inversement) ne peut pas être effectuée parfaitement dans chaque cas.

Utilisez les variables intégrales, BCD ou Currency pour éviter l'erreur de représentation à virgule flottante IEEE.

Compréhension du flux de données

En Delphi, les résultats intermédiaires des expressions à virgule flottante en simple précision sont toujours implicitement stockés en tant que Extended sur x86.

Par défaut, toutes les opérations et expressions arithmétiques x64 impliquant seulement des valeurs à virgule flottante en simple précision conservent une précision élevée en stockant les résultats intermédiaires sous la forme de valeurs à virgule flottante en double précision. En conséquence, ces opérations sont plus lentes que les opérations à virgule flottante en double précision (le code compilé convertit les valeurs à simple précision en valeurs à double précision sur chaque opération). Si la vitesse d'exécution est votre préoccupation principale, vous pouvez marquer le code en question avec la directive {$EXCESSPRECISION OFF} pour désactiver l'utilisation des valeurs intermédiaires en double précision. Sinon, la directive par défaut ({$EXCESSPRECISION ON}) est recommandée pour améliorer la précision de la valeur renvoyée. La directive {$EXCESSPRECISION OFF} ne fonctionne que pour le CPU cible x64.

Dans C++, un littéral à virgule flottante peut représenter un flottant en simple précision ou un flottant en double précision : cela dépend de la présence du suffixe f. Si f est ajouté à un littéral à virgule flottante dans C++, le compilateur choisit alors la simple précision. Pour comprendre la précision des valeurs intermédiaires, consultez les standards ISO publiés.

Les opérations à virgule flottante peuvent ne pas être associatives

En raison de l'erreur produite par chaque opérateur, l'ordre d'exécution des calculs est significatif.

Voir CERT C Secure Coding Standards, Recommendation FLP01-C (EN).

Exceptions de virgule flottante

Les opérations à virgule flottante peuvent conduire à plusieurs situations incorrectes comme le Débordement en virgule flottante, la division par zéro, la génération de NaNs, et l'exécution d'autres opérations à virgule flottante incorrectes. Généralement, de telles situations conduisent à déclencher des exceptions de virgule flottante. L'unité System.Math fournit :

Matériel de virgule flottante différent fournit différents moyens pour contrôler les exceptions de virgule flottante :

  • Sur les processeurs Intel 32 bits c'est le mot de contrôle FPU.
  • Sur les processeurs Intel 64 bits c'est le mot de contrôle SSE.
  • Nous ne supportons pas les exceptions de virgule flottante sur l'architecture ARM. Par conséquent, nous masquons toujours toutes les exceptions de virgule flottante sur l'architecture ARM.

Voir aussi