web-dev-qa-db-fra.com

Les mathématiques à virgule flottante sont-elles cohérentes en C #? Peut-il être?

Non, ce n'est pas une autre "Pourquoi est (1/3.0) * 3! = 1" question.

J'ai beaucoup lu sur les virgules flottantes récemment; en particulier, comment le même calcul peut donner des résultats différents sur différentes architectures ou paramètres d'optimisation.

C'est un problème pour les jeux vidéo qui stockent des rediffusions, ou sont peer-to-peer en résea (par opposition au serveur-client), qui dépendent de tous les clients générant exactement les mêmes résultats chaque fois qu'ils exécutent le programme - une petite différence dans un calcul à virgule flottante peut conduire à un état de jeu radicalement différent sur différentes machines (ou même sur la même machine! )

Cela se produit même parmi les processeurs qui "suivent" IEEE-754 , principalement parce que certains processeurs (à savoir x86) utilisent double précision étendue . Autrement dit, ils utilisent des registres de 80 bits pour effectuer tous les calculs, puis sont tronqués à 64 ou 32 bits, ce qui conduit à des résultats d'arrondi différents de ceux des machines qui utilisent 64 ou 32 bits pour les calculs.

J'ai vu plusieurs solutions à ce problème en ligne, mais toutes pour C++, pas C #:

  • Désactivez le mode double précision étendue (pour que tous les calculs double utilisent IEEE-754 64 bits) à l'aide de _controlfp_s (Windows), _FPU_SETCW (Linux?), Ou fpsetprec (BSD).
  • Exécutez toujours le même compilateur avec les mêmes paramètres d'optimisation et exigez que tous les utilisateurs aient la même architecture de processeur (pas de lecture multiplateforme). Parce que mon "compilateur" est en fait le JIT, qui peut optimiser différemment à chaque exécution du programme , je ne pense pas que ce soit possible.
  • Utilisez l'arithmétique à virgule fixe et évitez complètement float et double. decimal fonctionnerait à cette fin, mais serait beaucoup plus lent, et aucun des System.Math les fonctions de bibliothèque le supportent.

Donc, est-ce même un problème en C #? Et si j'ai seulement l'intention de supporter Windows (pas Mono)?

Si c'est le cas, existe-t-il un moyen de forcer mon programme à s'exécuter en double précision normale?

Sinon, y a-t-il des bibliothèques qui aideraient à garder les calculs à virgule flottante cohérents?

151

Je ne connais aucun moyen de rendre déterministe les virgules flottantes normales dans .net. Le JITter est autorisé à créer du code qui se comporte différemment sur différentes plates-formes (ou entre différentes versions de .net). Il n'est donc pas possible d'utiliser des floats normaux dans un code .net déterministe.

Les solutions de contournement que j'ai envisagées:

  1. Implémentez FixedPoint32 en C #. Bien que ce ne soit pas trop difficile (j'ai une implémentation à moitié terminée), la très petite plage de valeurs le rend ennuyeux à utiliser. Vous devez être prudent à tout moment afin de ne pas déborder ni perdre trop de précision. En fin de compte, je n'ai pas trouvé cela plus facile que d'utiliser directement des entiers.
  2. Implémentez FixedPoint64 en C #. J'ai trouvé cela assez difficile à faire. Pour certaines opérations, des nombres intermédiaires de 128 bits seraient utiles. Mais .net ne propose pas un tel type.
  3. Implémentez une virgule flottante 32 bits personnalisée. L'absence d'un intrinsèque BitScanReverse provoque quelques désagréments lors de sa mise en œuvre. Mais actuellement, je pense que c'est la voie la plus prometteuse.
  4. Utilisez du code natif pour les opérations mathématiques. Entraîne la surcharge d'un appel de délégué à chaque opération mathématique.

Je viens de commencer une implémentation logicielle de mathématiques à virgule flottante 32 bits. Il peut faire environ 70 millions d'ajouts/multiplications par seconde sur mon i3 à 2,66 GHz. https://github.com/CodesInChaos/SoftFloat . Évidemment, c'est encore très incomplet et buggé.

52
CodesInChaos

La spécification C # (§4.1.6 Types à virgule flottante) permet spécifiquement de faire des calculs en virgule flottante avec une précision supérieure à celle du résultat. Donc, non, je ne pense pas que vous puissiez rendre ces calculs déterministes directement dans .Net. D'autres ont suggéré différentes solutions de contournement, afin que vous puissiez les essayer.

27
svick

La page suivante peut être utile dans le cas où vous avez besoin d'une portabilité absolue de telles opérations. Il présente un logiciel pour tester les implémentations de la norme IEEE 754, y compris un logiciel pour émuler des opérations en virgule flottante. Cependant, la plupart des informations sont probablement spécifiques au C ou au C++.

http://www.math.utah.edu/~beebe/software/ieee/

Une note sur le point fixe

Les nombres binaires à virgule fixe peuvent également bien fonctionner comme substitut à la virgule flottante, comme le montrent les quatre opérations arithmétiques de base:

  • L'addition et la soustraction sont triviales. Ils fonctionnent de la même manière que les entiers. Il suffit d'ajouter ou de soustraire!
  • Pour multiplier deux nombres à virgule fixe, multipliez les deux nombres puis décalez vers la droite le nombre défini de bits fractionnaires.
  • Pour diviser deux nombres à virgule fixe, décaler le dividende vers la gauche du nombre défini de bits fractionnaires, puis diviser par le diviseur.
  • Le chapitre quatre de cet article contient des conseils supplémentaires sur la mise en œuvre des nombres binaires à virgule fixe.

Les nombres binaires à virgule fixe peuvent être implémentés sur tout type de données entier tel que int, long et BigInteger, et les types non conformes CLS uint et ulong.

Comme suggéré dans une autre réponse, vous pouvez utiliser des tables de recherche, où chaque élément de la table est un nombre à virgule fixe binaire, pour aider à implémenter des fonctions complexes telles que sinus, cosinus, racine carrée, etc. Si la table de recherche est moins granulaire que le nombre à virgule fixe, il est suggéré d'arrondir l'entrée en ajoutant une moitié de la granularité de la table de recherche à l'entrée:

// Assume each number has a 12 bit fractional part. (1/4096)
// Each entry in the lookup table corresponds to a fixed point number
//  with an 8-bit fractional part (1/256)
input+=(1<<3); // Add 2^3 for rounding purposes
input>>=4; // Shift right by 4 (to get 8-bit fractional part)
// --- clamp or restrict input here --
// Look up value.
return lookupTable[input];
14
Peter O.

Est-ce un problème pour C #?

Oui. Différentes architectures sont le moindre de vos soucis, différentes fréquences d'images, etc. peuvent entraîner des écarts dus à des inexactitudes dans les représentations flottantes - même si elles sont les mêmes inexactitudes ( par exemple, même architecture, sauf un GPU plus lent sur une seule machine).

Puis-je utiliser System.Decimal?

Il n'y a aucune raison pour laquelle vous ne pouvez pas, mais c'est un chien lent.

Existe-t-il un moyen de forcer mon programme à s'exécuter en double précision?

Oui. Hébergez vous-même le runtime CLR ; et compilez tous les appels/indicateurs nécessaires (qui modifient le comportement de l'arithmétique à virgule flottante) dans l'application C++ avant d'appeler CorBindToRuntimeEx.

Existe-t-il des bibliothèques qui permettraient de maintenir la cohérence des calculs en virgule flottante?

Pas que je sache de.

Existe-t-il un autre moyen de résoudre ce problème?

J'ai déjà abordé ce problème, l'idée est d'utiliser QNumbers . Ils sont une forme de réels à virgule fixe; mais pas de point fixe en base-10 (décimal) - plutôt en base-2 (binaire); de ce fait, les primitives mathématiques sur elles (add, sub, mul, div) sont beaucoup plus rapides que les points fixes naïfs de base 10; surtout si n est le même pour les deux valeurs (ce qui serait le cas dans votre cas). De plus, comme ils sont intégrés, ils ont des résultats bien définis sur chaque plate-forme.

Gardez à l'esprit que le framerate peut toujours les affecter, mais il n'est pas aussi mauvais et est facilement rectifié en utilisant des points de synchronisation.

Puis-je utiliser plus de fonctions mathématiques avec QNumbers?

Oui, aller-retour d'une décimale pour ce faire. De plus, vous devriez vraiment utiliser tables de recherche pour les fonctions trig (sin, cos); car ceux-ci peuvent vraiment donner des résultats différents sur différentes plateformes - et si vous les codez correctement, ils peuvent utiliser directement QNumbers.

9
Jonathan Dickinson

Selon ce légèrement ancien entrée de blog MSDN le JIT n'utilisera pas SSE/SSE2 pour virgule flottante, c'est tout x87. Pour cette raison, comme vous l'avez mentionné, vous devez vous soucier des modes et des indicateurs, et en C # ce n'est pas possible de contrôler. Ainsi, l'utilisation d'opérations normales en virgule flottante ne garantira pas le même résultat exact sur chaque machine pour votre programme.

Pour obtenir une reproductibilité précise de la double précision, vous devrez effectuer une émulation logicielle à virgule flottante (ou virgule fixe). Je ne connais pas les bibliothèques C # pour ce faire.

Selon les opérations dont vous avez besoin, vous pourrez peut-être vous en sortir avec une seule précision. Voici l'idée:

  • stocker toutes les valeurs qui vous intéressent en une seule précision
  • pour effectuer une opération:
    • étendre les entrées à la double précision
    • faire des opérations en double précision
    • convertir le résultat en précision simple

Le gros problème avec x87 est que les calculs peuvent être effectués avec une précision de 53 ou 64 bits selon l'indicateur de précision et si le registre s'est répandu en mémoire. Mais pour de nombreuses opérations, effectuer l'opération en haute précision et arrondir à une précision inférieure garantira la bonne réponse, ce qui implique que la réponse sera garantie d'être la même sur tous les systèmes. Que vous obteniez la précision supplémentaire n'aura pas d'importance, car vous avez suffisamment de précision pour garantir la bonne réponse dans les deux cas.

Opérations qui devraient fonctionner dans ce schéma: addition, soustraction, multiplication, division, sqrt. Des choses comme sin, exp, etc. ne fonctionneront pas (les résultats correspondent généralement mais il n'y a aucune garantie). "Quand le double arrondi est-il inoffensif?" Référence ACM (règlement rég. Requis)

J'espère que cela t'aides!

6
Nathan Whitehead

Comme déjà indiqué par d'autres réponses: Oui, c'est un problème en C # - même en restant Windows pur.

Quant à une solution: vous pouvez réduire (et avec un certain effort/impact sur les performances) éviter complètement le problème si vous utilisez la classe BigInteger intégrée et la mise à l'échelle de tous les calculs avec une précision définie en utilisant un dénominateur commun pour tout calcul/stockage de ces numéros.

Comme demandé par OP - concernant les performances:

System.Decimal représente un nombre avec 1 bit pour un signe et un entier de 96 bits et une "échelle" (représentant où se trouve le point décimal). Pour tous les calculs que vous effectuez, il doit fonctionner sur cette structure de données et ne peut utiliser aucune instruction à virgule flottante intégrée au CPU.

La BigInteger "solution" fait quelque chose de similaire - seulement que vous pouvez définir le nombre de chiffres dont vous avez besoin/voulez ... peut-être que vous voulez seulement 80 bits ou 240 bits de précision.

La lenteur vient toujours du fait de devoir simuler toutes les opérations sur ce nombre via des instructions entières uniquement sans utiliser les instructions intégrées CPU/FPU, ce qui conduit à son tour à beaucoup plus d'instructions par opération mathématique.

Pour réduire les performances, il existe plusieurs stratégies - comme QNumbers (voir la réponse de Jonathan Dickinson - Les mathématiques à virgule flottante sont-elles cohérentes en C #? Est-ce possible? ) et/ou la mise en cache (par exemple les calculs de trig. ..) etc.

4
Yahia

Eh bien, voici ma première tentative sur comment faire:

  1. Créez un projet ATL.dll qui contient un objet simple à utiliser pour vos opérations critiques en virgule flottante. assurez-vous de le compiler avec des indicateurs qui désactivent l'utilisation de tout matériel non xx87 pour faire des virgules flottantes.
  2. Créez des fonctions qui appellent des opérations en virgule flottante et renvoient les résultats; commencez simplement et si cela fonctionne pour vous, vous pouvez toujours augmenter la complexité pour répondre à vos besoins de performances plus tard si nécessaire.
  3. Mettez les appels control_fp autour du calcul réel pour vous assurer qu'il se fait de la même manière sur toutes les machines.
  4. Référencez votre nouvelle bibliothèque et testez pour vous assurer qu'elle fonctionne comme prévu.

(Je pense que vous pouvez simplement compiler en un fichier .dll 32 bits, puis l'utiliser avec x86 ou AnyCpu [ou probablement cibler uniquement x86 sur un système 64 bits; voir le commentaire ci-dessous].)

Ensuite, en supposant que cela fonctionne, si vous souhaitez utiliser Mono, j'imagine que vous devriez pouvoir répliquer la bibliothèque sur d'autres plates-formes x86 de manière similaire (pas COM bien sûr; bien que, peut-être, avec wine? Un peu hors de ma région une fois on y va quand même ...).

En supposant que vous puissiez le faire fonctionner, vous devriez être en mesure de configurer des fonctions personnalisées qui peuvent effectuer plusieurs opérations à la fois pour résoudre tous les problèmes de performances, et vous aurez des calculs en virgule flottante qui vous permettent d'avoir des résultats cohérents sur toutes les plateformes avec un montant minimal de code écrit en C++ et en laissant le reste de votre code en C #.

2
shelleybutterfly

Je ne suis pas un développeur de jeux, même si j'ai beaucoup d'expérience avec des problèmes de calcul difficiles ... alors je ferai de mon mieux.

La stratégie que j'adopterais est essentiellement la suivante:

  • Utilisez une méthode plus lente (si nécessaire; s'il existe un moyen plus rapide, parfait!), Mais prévisible pour obtenir des résultats reproductibles
  • Utilisez le double pour tout le reste (par exemple, le rendu)

Le short'n long de ceci est: vous devez trouver un équilibre. Si vous passez un rendu de 30 ms (~ 33 images par seconde) et seulement 1 ms pour la détection de collision (ou insérez une autre opération très sensible) - même si vous triplez le temps nécessaire pour effectuer l'arithmétique critique, l'impact que cela a sur votre débit d'images est vous passez de 33,3 ips à 30,3 ips.

Je vous suggère de tout profiler, de prendre en compte le temps passé à effectuer chacun des calculs sensiblement coûteux, puis de répéter les mesures avec une ou plusieurs méthodes pour résoudre ce problème et voir quel est l'impact.

2
Brian Vandenberg

En vérifiant les liens dans les autres réponses, il est clair que vous n'aurez jamais la garantie que la virgule flottante est "correctement" implémentée ou si vous recevrez toujours une certaine précision pour un calcul donné, mais peut-être pourriez-vous faire de votre mieux en (1) tronquer tous les calculs à un minimum commun (par exemple, si différentes implémentations vous donneront une précision de 32 à 80 bits, tronquant toujours chaque opération à 30 ou 31 bits), (2) avoir un tableau de quelques cas de test au démarrage (cas limites d'addition, de soustraction, de multiplication, de division, de sqrt, de cosinus, etc.) et si l'implémentation calcule des valeurs correspondant au tableau, ne vous embêtez pas à faire des ajustements.

1
mike