Le calcul à virgule flottante est-il cohérent dans C #? Peut-il être?

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

J’ai beaucoup lu sur les points flottants ces derniers temps. Plus précisément, comment un même calcul peut donner des résultats différents sur différentes architectures ou parameters d’optimisation.

Ceci est un problème pour les jeux vidéo qui stockent des replays ou qui sont en réseau peer-to-peer (par opposition à server-client), qui dépendent de tous les clients générant exactement les mêmes résultats à chaque exécution du programme. le calcul en 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 la précision double étendue . C’est-à-dire qu’ils utilisent des registres de 80 bits pour effectuer tous les calculs, puis tronquent à 64 ou 32 bits, ce qui conduit à des résultats d’arrondi différents de ceux des machines utilisant 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 (de sorte que tous double calculs double utilisent 64 bits IEEE-754) en utilisant _controlfp_s (Windows), _FPU_SETCW (Linux?) Ou fpsetprec (BSD).
  • Exécutez toujours le même compilateur avec les mêmes parameters d’optimisation et exigez que tous les utilisateurs aient la même architecture de processeur (pas de jeu multi-plateforme). Parce que mon “compilateur” est en fait le JIT, qui peut optimiser différemment chaque fois que le programme est exécuté , je ne pense pas que ce soit possible.
  • Utilisez l’arithmétique à virgule fixe et évitez les float et les double . decimal fonctionnerait à cette fin, mais serait beaucoup plus lent, et aucune des fonctions de la bibliothèque System.Math ne le prend en charge.

Donc, est-ce même un problème en C #? Que se passe-t-il si je ne souhaite que supporter Windows (pas Mono)?

Si c’est le cas, existe-t-il un moyen de forcer mon programme à fonctionner à une double précision normale?

Si ce n’est pas le cas, existe-t-il des bibliothèques permettant de maintenir la cohérence des calculs en virgule flottante?

Je ne connais aucun moyen de rendre les points flottants normaux déterministes dans .net. JITter est autorisé à créer un code qui se comporte différemment sur différentes plates-formes (ou entre différentes versions de .net). Donc, il n’est pas possible d’utiliser des float normaux dans un code .net déterministe.

Les solutions de contournement que j’ai considérées:

  1. Implémenter FixedPoint32 en C #. Bien que ce ne soit pas trop difficile (mon implémentation est à moitié terminée), la très petite gamme de valeurs la rend ennuyeuse à utiliser. Vous devez faire attention à tout moment pour ne pas déborder, ni perdre trop de précision. En fin de compte, j’ai trouvé que ce n’était pas plus facile que d’utiliser des entiers directement.
  2. Implémenter FixedPoint64 en C #. J’ai trouvé cela assez difficile à faire. Pour certaines opérations, des entiers intermédiaires de 128 bits seraient utiles. Mais .net n’offre pas un tel type.
  3. Implémentez un point flottant 32 bits personnalisé. L’absence de valeur insortingnsèque de BitScanReverse entraîne quelques désagréments lors de la mise en œuvre de cette solution. Mais actuellement, je pense que c’est la voie la plus prometteuse.
  4. Utilisez le code natif pour les opérations mathématiques. Affecte la surcharge d’un appel de délégué à chaque opération mathématique.

Je viens de commencer une implémentation logicielle de calcul 32 bits en virgule flottante. Il peut faire environ 70 millions d’additions / multiplications par seconde sur mon i3 de 2,66 GHz. https://github.com/CodesInChaos/SoftFloat . Evidemment c’est toujours très incomplet et buggy.

La spécification C # (§ 4.1.6 Types de virgule flottante) permet spécifiquement d’effectuer des calculs en virgule flottante 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 dans .Net. D’autres ont suggéré diverses solutions de contournement pour pouvoir les essayer.

La page suivante peut être utile dans le cas où vous avez besoin d’une portabilité absolue de telles opérations. Il aborde les logiciels permettant de tester les implémentations de la norme IEEE 754, notamment les logiciels permettant d’émuler les opérations à virgule flottante. La plupart des informations sont probablement spécifiques à C ou C ++.

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

Une note sur le point fixe

Les nombres binarys à virgule fixe peuvent également servir de substitut à la virgule flottante, comme en témoignent les quatre opérations arithmétiques de base:

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

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

Comme suggéré dans une autre réponse, vous pouvez utiliser des tables de consultation, où chaque élément de la table est un nombre à virgule fixe, pour vous aider à implémenter des fonctions complexes telles que sinus, cosinus, racine carrée, etc. Si la table de consultation est moins granulaire que le nombre à virgule fixe, il est conseillé de 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 ressortingct input here -- // Look up value. return lookupTable[input]; 

Est-ce un problème pour C #?

Oui. Différentes architectures sont les moindres de vos soucis, différentes images-frameworks, etc. peuvent entraîner des écarts dus à des inexactitudes dans les représentations flottantes – même si elles sont identiques (par exemple, même architecture, sauf un GPU plus lent sur une machine).

Puis-je utiliser System.Decimal?

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

Est-il possible de forcer mon programme à fonctionner en double précision?

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

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

Pas que je sache de.

Y a-t-il une autre façon 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 qui sont à virgule fixe; mais pas point fixe en base 10 (décimal) – plutôt base 2 (binary); de ce fait, les primitives mathématiques sur elles (add, sub, mul, div) sont beaucoup plus rapides que les points fixes de base-10 naïfs; surtout si n est le même pour les deux valeurs (ce qui dans votre cas serait le même). De plus, du fait de leur intégrité, 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 il est facile de le corriger en utilisant des points de synchronisation.

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

Oui, faites un tour décimal pour faire cela. De plus, vous devriez vraiment utiliser des tables de consultation pour les fonctions sortingg (sin, cos); comme ceux-ci peuvent donner des résultats différents sur différentes plates-formes – et si vous les codez correctement, ils peuvent utiliser directement QNumbers.

Selon cette entrée de blog MSDN un peu ancienne, JIT n’utilisera pas SSE / SSE2 pour les virgules flottantes, mais x87. Pour cette raison, comme vous l’avez mentionné, vous devez vous préoccuper des modes et des indicateurs, et en C #, il est impossible de contrôler. L’utilisation des opérations à virgule flottante normales ne garantit donc pas le même résultat sur toutes les machines de votre programme.

Pour obtenir une reproductibilité précise de la double précision, vous devrez faire 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 dont vous vous souciez en simple précision
  • pour effectuer une opération:
    • étendre les entrées pour doubler la précision
    • faire l’opération en double précision
    • convertir le résultat en simple précision

Le gros problème avec x87 est que les calculs peuvent être effectués avec une précision de 53 bits ou 64 bits, en fonction de l’indicateur de précision et de la mémoire du registre. Mais pour de nombreuses opérations, effectuer l’opération en haute précision et arrondir à une précision inférieure garantira la réponse correcte, ce qui implique que la réponse sera garantie sur tous les systèmes. Que vous obteniez la précision supplémentaire n’aura aucune importance, puisque 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 le péché, l’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 (reg. Req. Payée)

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 sous Windows.

En ce qui concerne une solution: vous pouvez réduire (et avec un certain effort / performance) éviter complètement le problème si vous utilisez la classe BigInteger et redimensionnez tous les calculs à une précision définie en utilisant un dénominateur commun pour tout calcul / stockage de ces nombres .

Comme demandé par OP – concernant les performances:

System.Decimal représente le nombre avec 1 bit pour un signe et 96 bits Integer et une “échelle” (représentant où le point décimal est). Pour tous les calculs, vous devez utiliser cette structure de données et ne pouvez utiliser aucune instruction à virgule flottante intégrée à la CPU.

La “solution” de BigInteger fait quelque chose de similaire – seulement que vous pouvez définir la quantité de chiffres dont vous avez besoin / que vous voulez … peut-être que vous ne voulez que 80 bits ou 240 bits de précision.

La lenteur vient toujours de devoir simuler toutes les opérations sur ces nombres via des instructions uniquement entières sans utiliser les instructions intégrées à la CPU / FPU, ce qui conduit à 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 #? Peut-il être? ) Et / ou la mise en cache (par exemple les calculs sortingg …)

Eh bien, voici ma première tentative pour y parvenir :

  1. Créez un projet ATL.dll contenant un object simple à utiliser pour vos opérations critiques en virgule flottante. assurez-vous de le comstackr avec des indicateurs qui désactivent tout matériel non xx87 pour effectuer des opérations en virgule flottante.
  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 performance plus tard si nécessaire.
  3. Mettez les appels control_fp autour des mathématiques réelles pour vous assurer que cela 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 crois que vous pouvez simplement comstackr en .dll 32 bits, puis l’utiliser avec x86 ou AnyCpu [ou ne cibler probablement que x86 sur un système 64 bits; voir commentaire ci-dessous].)

Puis, en supposant que cela fonctionne, si vous voulez utiliser Mono, j’imagine que vous devriez être capable de 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? nous y allons cependant …).

En supposant que vous puissiez le faire fonctionner, vous devriez être capable de configurer des fonctions personnalisées capables d’effectuer plusieurs opérations en même temps pour résoudre les problèmes de performances, et vous obtiendrez des résultats cohérents sur plusieurs plates-formes avec une quantité minimale du code écrit en C ++, et en laissant le rest 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 informatiques difficiles, alors je ferai de mon mieux.

La stratégie que j’adopterais est essentiellement celle-ci:

  • Utilisez une méthode plus lente (si nécessaire, s’il y a un moyen plus rapide, génial!), Mais prévisible pour obtenir des résultats reproductibles
  • Utilisez le double pour tout le rest (par exemple, le rendu)

En bref: 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 sortinglez le temps qu’il faut pour effectuer l’arithmétique critique, l’impact sur votre fréquence d’images est vous tombez de 33.3fps à 30.3fps.

Je vous suggère de tout profiler, comptez combien de temps est passé pour chacun des calculs sensiblement coûteux, puis répétez les mesures avec une ou plusieurs méthodes de résolution de ce problème et voyez l’impact.

En vérifiant les liens dans les autres réponses, vous avez la certitude de ne jamais savoir si un virgule flottante est “correctement” implémentée ou si vous obtenez toujours une certaine précision pour un calcul donné. (1) tronquer tous les calculs à un minimum commun (par exemple, si différentes implémentations vous donneront une précision de 32 à 80 bits, en tronquant toujours chaque opération à 30 ou 31 bits), (2) (cas limites de add, soustraire, multiplier, diviser, sqrt, cosinus, etc.) et si l’implémentation calcule des valeurs correspondant à la table, alors il ne faut pas faire de réglages.

Votre question est assez difficile et technique O_o. Cependant, je peux avoir une idée.

Vous savez certainement que le processeur effectue des ajustements après toute opération flottante. Et CPU offre plusieurs instructions différentes qui font des opérations d’arrondi différentes.

Donc, pour une expression, votre compilateur choisira un ensemble d’instructions qui vous mènera à un résultat. Mais tout autre stream de travail d’instruction, même s’il a l’intention de calculer la même expression, peut fournir un autre résultat.

Les «erreurs» d’un ajustement d’arrondi augmenteront à chaque nouvelle instruction.

A titre d’exemple, on peut dire qu’au niveau de l’assemblage: a * b * c n’est pas équivalent à a * c * b.

Je ne suis pas tout à fait sûr de cela, vous devrez demander à quelqu’un qui connaît l’architecture du processeur beaucoup plus que moi: p

Cependant, pour répondre à votre question: en C ou C ++, vous pouvez résoudre votre problème car vous avez un certain contrôle sur le code machine généré par votre compilateur, mais dans .NET, vous n’en avez aucun. Donc, tant que votre code machine peut être différent, vous ne serez jamais sûr du résultat exact.

Je suis curieux de savoir comment cela peut être un problème car la variation semble très minime, mais si vous avez besoin d’un fonctionnement vraiment précis, la seule solution à mon avis sera d’augmenter la taille de vos registres flottants. Utilisez la double précision ou même le double long si vous le pouvez (pas sûr que ce soit possible avec l’interface de ligne de commande).

J’espère que j’ai été assez clair, je ne suis pas parfait en anglais (… du tout: s)