Est-ce que les maths à virgule flottante sont cohérentes dans C#? Peut-il être?
non, ce n'est pas un autre "Pourquoi est (1/3.0) * 3 != 1" question.
j'ai beaucoup lu sur les floating-points récemment; plus précisément, comment le même calcul pourrait 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 replays, ou sont peer-to-peer en réseau (par opposition à 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 divergence dans un calcul flottant peut conduire à un État de jeu radicalement différent sur des machines différentes (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 étendu la précision . C'est-à - dire qu'ils utilisent des registres à 80 bits pour faire tous les calculs, puis se tronquent à 64 bits ou 32 bits, conduisant à des résultats d'arrondi différents que les machines qui utilisent 64 bits ou 32 bits pour les calculs.
j'ai vu plusieurs solutions à ce problème en ligne, mais tout pour C++, pas C#:
- désactiver le mode double précision (de sorte que tous les calculs
double
utilisent IEEE-754 64 bits) en utilisant_controlfp_s
(Windows),_FPU_SETCW
(Linux?), oufpsetprec
(BSD). - exécute toujours le même compilateur avec les mêmes paramètres d'optimisation, et demande à tous les utilisateurs d'avoir la même architecture CPU (pas de jeu multiplateforme). Parce que mon "compilateur" est en fait le JIT, que peut optimiser différemment chaque fois que le programme est lancé , Je ne pense pas que ce soit possible.
- Use arithmétique à point fixe, et éviter
float
etdouble
tout à fait.decimal
fonctionnerait dans ce but, mais serait beaucoup plus lent, et aucune des fonctions de la bibliothèqueSystem.Math
ne le supporte.
donc, est-ce même un problème en C#? Que faire si je n'ai l'intention de prendre en charge que Windows (et non Mono)?
si c'est le cas, y a-t-il un moyen de forcer mon programme à fonctionner normalement? double précision?
si non, y a-t-il des bibliothèques qui pourraient aider à maintenir la cohérence des calculs à virgule flottante?
10 réponses
Je ne connais aucun moyen de rendre les points flottants normaux déterministes .net. Le JITter est autorisé à créer du code qui se comporte différemment sur différentes plateformes(ou entre différentes versions de .net). Il n'est donc pas possible d'utiliser le float
s normal dans le code déterministe.net.
Les solutions de contournement, j'ai réfléchi:
- mettre en Œuvre FixedPoint32 en C#. Bien que ce ne soit pas trop difficile(j'ai à moitié terminé la mise en œuvre) le très petite plage de valeurs en fait ennuyeux à utiliser. Vous devez être prudent en tout temps pour ne pas déborder, ni perdre trop de précision. En fin de compte j'ai trouvé ce pas plus facile que d'utiliser des entiers directement.
- mettre en Œuvre FixedPoint64 en C#. J'ai trouvé ça plutôt difficile à faire. Pour certaines opérations intermédiaires entiers de 128 bits serait utile. Mais .net n'offre pas un tel type.
- implémenter un point de flottaison 32 bits personnalisé. Le manque D'un BitScanReverse intrinsèque provoque quelques désagréments lors de la mise en œuvre de cette. Mais actuellement, je pense que c'est la voie la plus prometteuse.
- utilisez le code natif pour les opérations mathématiques. Engage les frais généraux d'un délégué à chaque opération mathématique.
je viens de commencer une implémentation logicielle de 32 bits de calcul à virgule flottante. Il peut faire environ 70millions d'additions / multiplications par seconde sur mon i3 2.66 GHz. https://github.com/CodesInChaos/SoftFloat . Évidemment, il est encore très incomplet et buggy.
la spécification C# (§4.1.6 Floating point types) permet spécifiquement que les calculs en virgule flottante soient effectués avec une précision supérieure à celle du résultat. Donc, Non, Je ne pense pas que vous pouvez rendre ces calculs déterministes directement .Net. D'autres ont suggéré diverses solutions de rechange, pour que vous puissiez les essayer.
la page suivante peut être utile dans le cas où vous avez besoin de portabilité absolue de telles opérations. Il traite des logiciels pour tester les implémentations de la norme IEEE 754, y compris les logiciels pour émuler les opérations flottantes. Cependant, la plupart des informations sont probablement spécifiques à C ou c++.
http://www.math.utah.edu/~beebe/logiciel/ieee/
note sur point fixe
Binaire point fixe de numéros qui peuvent aussi bien fonctionner comme un substitut à virgule flottante, comme en témoignent les quatre opérations arithmétiques de base:
- L'Addition et la soustraction sont sans importance. Ils fonctionnent de la même façon que les nombres entiers. Juste d'ajouter ou de soustraire!
- pour multiplier deux nombres à points fixes, multiplier les deux nombres puis décaler à droite le nombre défini de bits fractionnels.
- pour diviser deux points fixes nombres, déplacer le dividende laissé le nombre défini de bits fractionnaires, puis diviser par le diviseur.
- chapitre quatre de le présent document contient des directives supplémentaires sur la mise en œuvre de nombres binaires à points fixes.
les nombres binaires à points fixes peuvent être implémentés sur n'importe quel type de données integer comme int, long, et BigInteger, et les types non conformes au CLS uint et ulong.
comme suggéré dans une autre réponse, vous pouvez utiliser des tables de recherche, où chaque élément dans la table est un nombre fixe binaire de points, pour aider à mettre en œuvre des fonctions complexes telles que sine, cosine, racine carrée, et ainsi de suite. Si la table de recherche est moins granulaire que le nombre de point fixe, il est suggéré d'arrondir l'entrée en ajoutant la 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érents framerates, etc. peut conduire à des déviations dues à des inexactitudes dans les représentations de flotteurs - même s'il s'agit des imprécisions même (par exemple, même architecture, sauf un GPU plus lent sur une machine).
puis-je utiliser le système.Décimal?
il n'y a pas raison vous ne pouvez pas, mais il est chien lent.
y a-t-il un moyen de forcer mon programme à fonctionner avec une double précision?
Oui. héberger l'exécution CLRTIME vous-même ; et compiler dans tous les appels/drapeaux nessecary (qui changent le comportement de l'arithmétique flottante) dans l'application C++ avant D'appeler CorBindToRuntimeEx.
y a-t-il des bibliothèques qui pourraient vous aider à garder les calculs à la virgule flottante sont-ils cohérents?
Pas que je sache.
y a-t-il une autre solution?
j'ai déjà abordé ce problème, l'idée est d'utiliser QNumbers . Ils sont une forme de réals qui sont à point fixe; mais pas Point Fixe en base-10 (décimal) - plutôt base-2 (binaire); en raison de cela les primitives mathématiques sur eux (add, sub, mul, div) sont beaucoup plus rapides que la base naïve-10 points fixes; surtout si n
est le même pour les deux valeurs (qui dans votre cas, il serait). En outre, parce qu'ils sont intégraux, ils ont des résultats bien définis sur chaque plate-forme.
gardez à l'esprit que framerate peut encore affecter ceux-ci, mais il n'est pas aussi mauvais et est facilement rectifié en utilisant des points de syncronisation.
puis-je utiliser plus de fonctions mathématiques avec QNumbers?
Oui, aller-retour une décimale pour faire ça. En outre, vous devriez vraiment utiliser tables de recherche pour les fonctions trig (sin, cos); Comme ceux - ci peuvent vraiment donner des résultats différents sur différentes plates-formes-et si vous les coder correctement, ils peuvent utiliser QNumbers directement.
selon ce légèrement Vieux MSDN blog entry le JIT ne va pas utiliser SSE / SSE2 pour flottant point, il est tout x87. Pour cette raison, comme vous l'avez mentionné, vous devez vous soucier des modes et des drapeaux, et dans C# c'est impossible à contrôler. Ainsi, l'utilisation d'opérations flottantes normales ne garantira pas le même résultat exact sur chaque machine pour votre programme.
pour obtenir une reproductibilité précise de double précision vous allez devoir faire émulation point flottant (ou point fixe) du logiciel. Je ne connais pas de bibliothèques C# pour faire ça.
Selon les opérations dont vous avez besoin, vous pourriez être en mesure de s'en tirer avec précision unique. Voici l'idée:
- stocker toutes les valeurs dont vous vous souciez en une seule précision
- pour effectuer une opération:
- développer les entrées en double précision
- do opération en double précision
- convertissez le résultat de retour à la précision simple
le gros problème avec x87 est que les calculs peuvent être effectués avec une précision de 53 bits ou de 64 bits, selon l'indicateur de précision et le fait que le registre soit passé en mémoire. Mais pour de nombreuses opérations, l'exécution de l'opération en haute précision et en arrondissant de nouveau à une plus faible précision garantira la réponse correcte, ce qui implique que la réponse sera garanti d'être de la même manière sur tous les systèmes. Que vous obteniez la précision supplémentaire n'importe pas, puisque vous avez assez 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 le péché, exp, etc. ne fonctionne pas (les résultats concordent généralement, mais il n'y a aucune garantie). " quand le double arrondi est-il inoffensif?"Référence ACM (paid reg. req.)
Espérons que cette aide!
comme déjà indiqué par d'autres réponses: Oui, c'est un problème en C# - même en restant des fenêtres pures.
comme solution:
Vous pouvez réduire (et avec un certain effort/performance hit) éviter le problème complètement si vous utilisez intégré BigInteger
classe et mise à l'échelle de tous les calculs à une précision définie en utilisant un dénominateur commun pour tout calcul/stockage de tels nombres.
Comme demandé par l'OP - en ce qui concerne 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ù le point décimal est). Pour tous les calculs que vous faites, il doit fonctionner sur cette structure de données et ne peut pas utiliser d'instructions flottantes intégrées dans le CPU.
la BigInteger
" solution " fait quelque chose de similaire - seulement que vous pouvez définir combien de chiffres vous avez besoin/veulent... peut-être voulez-vous seulement 80 ou 240 bits de précision.
la lenteur vient toujours d'avoir à simuler toutes les opérations sur ces nombres par des instructions entières seulement sans utiliser les instructions CPU/FPU-intégré qui à son tour conduit à beaucoup plus d'instructions par opération mathématique.
pour réduire le succès de la performance il y a plusieurs stratégies-comme QNumbers (voir la réponse de Jonathan Dickinson - est-ce que les mathématiques à virgule flottante sont cohérentes dans C#? Peut-il être? ) et / ou la mise en cache (par exemple trig calcul...) etc.
Eh bien, voici ma première tentative sur comment faire ce :
- créer un ATL.projet dll qui a un objet simple en elle pour être utilisé pour vos opérations critiques de point flottant. assurez-vous de le compiler avec des options qui désactivent l'utilisation de n'importe quel matériel non xx87 pour faire floating point.
- créer des fonctions qui appellent des opérations en virgule flottante et retourner les résultats; commencer simple et puis si cela fonctionne pour vous, vous pouvez toujours augmenter la complexité pour répondre à vos besoins de performance, plus tard, si nécessaire.
- mettez les appels control_fp autour du calcul réel pour s'assurer que c'est fait de la même manière sur toutes les machines.
- référez votre nouvelle bibliothèque et testez pour vous assurer qu'elle fonctionne comme prévu.
(je crois que vous pouvez simplement compiler à un 32-bit .dll et ensuite l'utiliser avec x86 ou AnyCpu [ou vraisemblablement ciblant seulement x86 sur un système 64-bit; voir le commentaire ci-dessous].)
alors, en supposant que cela fonctionne, si vous voulez utiliser Mono j'imagine que vous devriez être en mesure de répliquer la bibliothèque sur d'autres plates-formes x86 d'une manière similaire (pas COM, bien sûr; bien que, peut-être, avec du vin? un peu hors de ma zone une fois que nous y allons...).
en supposant que vous pouvez le faire fonctionner, vous devriez être en mesure de mettre en place des fonctions personnalisées qui peuvent faire plusieurs opérations à la fois pour corriger les problèmes de performance, et vous aurez les mathématiques à virgule flottante qui vous permettent d'avoir des résultats cohérents entre les plates-formes avec une quantité minimale de code écrit en C++, et laissant le reste de votre code en C#.
Je ne suis pas un développeur de jeux, bien que j'ai beaucoup d'expérience avec des problèmes de calcul difficiles ... donc, je vais faire de mon mieux.
la stratégie que j'adopterais est essentiellement la suivante:
- utilisez un plus lent (si nécessaire; s'il y a un moyen plus rapide, parfait!), mais méthode prévisible pour obtenir des résultats reproductibles
- utiliser le double pour tout le reste (par exemple, le rendu)
court - long de ceci est: vous devez trouver un équilibre. Si vous passez 30ms de rendu (~33fps) et seulement 1ms de détection de collision (ou insérez une autre opération très sensible) -- même si vous triplez le temps qu'il faut pour faire l'arithmétique critique, l'impact qu'il a sur votre framerate est que vous passez de 33,3 fps à 30,3 fps.
je vous suggère de tout profiler, de tenir compte du temps passé à faire chacun des calculs remarquablement coûteux, puis de répéter les mesures avec 1 ou plusieurs méthodes pour résoudre ce problème et voir ce que l'impact est.
vérifier les liens dans les autres réponses, il est clair que vous n'aurez jamais une garantie de savoir si la virgule flottante est "correctement" mis en œuvre ou si vous recevrez toujours une certaine précision pour un calcul donné, mais peut-être que vous pourriez faire un meilleur effort en (1) tronquer tous les calculs à un minimum commun (par exemple, si différentes implémentations vous donnera 32 à 80 bits de précision, toujours tronquer chaque opération à 30 ou 31 bits), (2) avoir un tableau de quelques cas d'essai à de démarrage (cas limites d'ajouter, soustraire, multiplier, diviser, sqrt, cosinus, etc.) et si l'implémentation calcule des valeurs correspondant à la table alors pas la peine de faire des ajustements.
votre question en matière assez difficile et technique O_o. Mais j'ai peut-être une idée.
vous savez certainement que le CPU fait quelques ajustements après toute opération flottante. Et CPU offrent plusieurs instructions différentes qui font des opérations d'arrondissement différentes.
ainsi, pour une expression, votre compilateur choisira un ensemble d'instructions qui vous mèneront à un résultat. Mais tout autre flux d'instruction, même s'ils ont l'intention de calculer le même d'expression, peut fournir un autre résultat.
les "erreurs" faites par un ajustement d'arrondissement augmenteront à chaque nouvelle instruction.
par exemple, on peut dire qu'au niveau de l'assemblage: a * b * c n'est pas équivalent à A * C * b.
Je ne suis pas entièrement sûr de cela, vous aurez besoin de demander quelqu'un qui connaît l'architecture CPU beaucoup plus que moi : p
Cependant pour répondre à votre question: en C ou C++ vous pouvez résoudre votre problème parce que vous avez un certain contrôle sur le code machine généré par votre compilateur, cependant dans .NET vous n'en avez pas. Aussi longtemps que votre code machine peut être différent, vous ne serez jamais sûr du résultat exact.
je suis curieux de savoir de quelle façon cela peut être un problème parce que la variation semble très minime, mais si vous avez besoin d'un fonctionnement vraiment précis, la seule solution que je peux penser sera d'augmenter la taille de vos registres flottants. Utilisez une double précision ou même long double si vous le pouvez (pas sûr que ce soit possible à l'aide de la CLI).
j'espère que j'ai été assez clair, je ne suis pas parfait en anglais (...: s)