Quelque chose dans C # peut-il changer le comportement de la comparaison flottante au moment de l’exécution?

Je rencontre un problème très étrange avec notre code de test dans un produit commercial sous Windows 7 64 bits avec VS 2012 .net 4.5 compilé en 64 bits.

Le code de test suivant, lorsqu’il est exécuté dans un projet séparé, se comporte comme prévu (à l’aide d’un programme d’exécution de test NUnit):

[Test] public void Test() { float x = 0.0f; float y = 0.0f; float z = 0.0f; if ((x * x + y * y + z * z) < (float.Epsilon)) { return; } throw new Exception("This is totally bad"); } 

Le test retourne car la comparaison avec <float.Epsilon est toujours vraie pour x, y et z étant 0.0f.

Maintenant, voici la partie étrange. Lorsque je lance ce code dans le contexte de notre produit commercial, ce test échoue. Je sais à quel point cela peut sembler stupide, mais je vais faire exception. J’ai débogué le problème et lorsque j’évalue la condition, il est toujours vrai, mais l’exécutable compilé ne va toujours pas dans la twig vraie de la condition et lève l’exception.

Dans le produit commercial, ce scénario de test échoue uniquement lorsque mon scénario de test utilise un code de configuration supplémentaire (conçu pour les tests d’intégration) lorsqu’un très grand système est initialisé (C #, CLI et une très grande partie C ++). Je ne peux pas creuser plus loin dans cet appel car il lance pratiquement tout.

Je ne suis au courant d’aucun élément en C # susceptible d’influencer une évaluation.

Bonus étrange: Quand je compare avec moins-ou-égal avec float.Epsilon:

 if ((x * x + y * y + z * z) <= (float.Epsilon)) // this works! 

alors le test réussit. J’ai essayé de comparer seulement avec less-than et float.Epsilon * 10 mais cela n’a pas fonctionné:

 if ((x * x + y * y + z * z) < (float.Epsilon*10)) // this doesn't! 

Je n’ai pas réussi à googler cette question et même si les publications d’Eric Lippert et al. J’ai tendance à aller vers float.Epsilon Je ne comprends pas bien quel effet est appliqué sur mon code. S’agit-il d’un paramètre C #, l’important système natif à géré et inversement influence-t-il le système? Quelque chose dans la CLI?

Edit: Quelques autres choses à découvrir: J’ai utilisé GetComponentParts à partir de cette page MSDN http://msdn.microsoft.com/en-us/library/system.single.epsilon%28v=vs.110%29.aspx pour visualiser mon mantiassa end exposants et voici les résultats:

Code de test:

  float x = 0.0f; float y = 0.0f; float z = 0.0f; var res = (x*x + y*y + z*z); Console.WriteLine(GetComponentParts(res)); Console.WriteLine(); Console.WriteLine(GetComponentParts(float.Epsilon)); 

Je ne reçois pas toute la chaîne de boostrap (test réussi)

 0: Sign: 0 (+) Exponent: 0xFFFFFF82 (-126) Mantissa: 0x0000000000000 1.401298E-45: Sign: 0 (+) Exponent: 0xFFFFFF82 (-126) Mantissa: 0x0000000000001 

Je reçois une chaîne d’amorçage complète (le test échoue)

 0: Sign: 0 (+) Exponent: 0xFFFFFF82 (-126) Mantissa: 0x0000000000000 0: Sign: 0 (+) Exponent: 0xFFFFFF82 (-126) Mantissa: 0x0000000000000 

Les choses à noter: Le float.Epsilon a perdu son dernier bit dans sa mantisse.

Je ne vois pas comment le drapeau du compilateur / fp en C ++ a une influence sur la représentation float.Epsilon.


Edit et verdict final Bien qu’il soit possible d’utiliser un thread séparé pour obtenir float.Epsilon, son comportement sera différent de celui attendu sur le thread avec un mot FPU réduit.

Sur le mot FPU réduit, thread, il s’agit de la sortie du float.Epsilon “thread-foreign”.

 0: Sign: 0 (+) Exponent: 0xFFFFFF82 (-126) Mantissa: 0x0000000000001 

Notez que le dernier bit de mantisse correspond à 1 comme prévu, mais que cette valeur flottante sera toujours interprétée comme étant 0. Ceci est bien sûr logique car nous utilisons la précision d’un flottant plus grand que le jeu de mots FPU, mais cela peut être un piège pour quelqu’un

J’ai décidé de passer à une machine fps calculée une fois, comme décrit ici: https://stackoverflow.com/a/9393079/2416394 (porté à float, bien sûr)

DirectX est connu pour modifier les parameters FPU. Voir cette question connexe: La précision en virgule flottante peut-elle dépendre du thread?

Vous pouvez demander à DirectX de conserver les parameters FPU en spécifiant l’indicateur D3DCREATE_FPU_PRESERVE lors de l’appel de CreateDevice ou d’exécuter votre code à virgule flottante sur un nouveau thread.

Si vous obtenez des différences entre le moment où vous déboguez et le moment où il s’exécute en mode de publication, vous risquez de tomber sous le coup des erreurs suivantes:

(Extrait de MS Partition I , 12.1.3):

Les emplacements de stockage des nombres à virgule flottante (statique, éléments de tableau et champs de classes) ont une taille fixe … Partout ailleurs (sur la stack d’évaluation, en tant qu’arguments, en tant que types de retour et en tant que variables locales) représenté à l’aide d’un type à virgule flottante interne. … sa valeur peut être représentée en interne avec une scope et / ou une précision supplémentaires

et,

Lorsqu’une valeur à virgule flottante dont la représentation interne a une plage et / ou une précision supérieures à celle de son type nominal est placée dans un emplacement de stockage, elle est automatiquement contrainte au type de l’emplacement de stockage. Cela peut impliquer une perte de précision ou la création d’une valeur hors de scope

et la note finale:

[ Remarque: l’utilisation d’une représentation interne plus large que float32 ou float64 peut entraîner des différences dans les résultats de calcul lorsqu’un développeur apporte à son code des modifications apparemment sans rapport, ce qui peut avoir pour conséquence qu’une valeur soit déversée de la représentation interne (par exemple: , dans un registre) à un emplacement de la stack. note de fin ]

Le débogage entraîne généralement de nombreuses modifications – vous avez tendance à utiliser différentes optimisations et vous êtes plus susceptible de provoquer de tels débordements.

Lorsque je crée une application de test contenant votre test, l’exception n’est pas levée. Cela signifie que ça ne va pas être simple. Quelques idées à approfondir:

  • Si vous exécutez ce test au début de votre application (par exemple, dans la routine Main / entry), est-ce qu’il échoue?
  • Si ce qui précède est vrai, démarrez un nouveau projet en utilisant le même framework et architecture cible qui exécute le même test. Si cela réussit, commencez à append progressivement des bits de votre application principale pour voir si vous pouvez trouver quel bit l’échoue.