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 #:
double
utilisent IEEE-754 64 bits) à l'aide de _controlfp_s
(Windows), _FPU_SETCW
(Linux?), Ou fpsetprec
(BSD).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?
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 float
s normaux dans un code .net déterministe.
Les solutions de contournement que j'ai envisagées:
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é.
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.
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:
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];
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.
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:
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!
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.
Eh bien, voici ma première tentative sur comment faire:
(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 #.
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:
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.
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.