Pourquoi le résultat de Vector2.Normalize () change-t-il après l'avoir appelé 34 fois avec des entrées identiques?

10

Voici un simple programme C # .NET Core 3.1 qui appelle System.Numerics.Vector2.Normalize()en boucle (avec une entrée identique à chaque appel) et imprime le vecteur normalisé résultant:

using System;
using System.Numerics;
using System.Threading;

namespace NormalizeTest
{
    class Program
    {
        static void Main()
        {
            Vector2 v = new Vector2(9.856331f, -2.2437377f);
            for(int i = 0; ; i++)
            {
                Test(v, i);
                Thread.Sleep(100);
            }
        }

        static void Test(Vector2 v, int i)
        {
            v = Vector2.Normalize(v);
            Console.WriteLine($"{i:0000}: {v}");
        }
    }
}

Et voici le résultat de l'exécution de ce programme sur mon ordinateur (tronqué par souci de concision):

0000: <0.9750545, -0.22196561>
0001: <0.9750545, -0.22196561>
0002: <0.9750545, -0.22196561>
...
0031: <0.9750545, -0.22196561>
0032: <0.9750545, -0.22196561>
0033: <0.9750545, -0.22196561>
0034: <0.97505456, -0.22196563>
0035: <0.97505456, -0.22196563>
0036: <0.97505456, -0.22196563>
...

Donc , ma question est, pourquoi le résultat de l' appel Vector2.Normalize(v)changement de <0.9750545, -0.22196561>à <0.97505456, -0.22196563>après l' appeler 34 fois? Est-ce attendu ou s'agit-il d'un bogue dans la langue / le runtime?

Walt D
la source
Les flotteurs sont étranges
Milney
2
@Milney Peut-être, mais ils sont aussi déterministes . Ce comportement ne s'explique pas uniquement par le fait que les flotteurs sont étranges.
Konrad Rudolph

Réponses:

14

Ma question est donc la suivante: pourquoi le résultat de l'appel de Vector2.Normalize (v) passe-t-il de <0,9750545, -0,22196561> à <0,97505456, -0,22196563> après l'avoir appelé 34 fois?

Alors d'abord - pourquoi le changement se produit. La modification est observée car le code qui calcule ces valeurs change également.

Si nous entrons dans WinDbg au début des premières exécutions du code et descendons un peu dans le code qui calcule le Normalizevecteur ed, nous pourrions voir l'assemblage suivant (plus ou moins - j'ai coupé certaines parties):

movss   xmm0,dword ptr [rax]
movss   xmm1,dword ptr [rax+4]
lea     rax,[rsp+40h]
movss   xmm2,dword ptr [rax]
movss   xmm3,dword ptr [rax+4]
mulss   xmm0,xmm2
mulss   xmm1,xmm3
addss   xmm0,xmm1
sqrtss  xmm0,xmm0
lea     rax,[rsp+40h]
movss   xmm1,dword ptr [rax]
movss   xmm2,dword ptr [rax+4]
xorps   xmm3,xmm3
movss   dword ptr [rsp+28h],xmm3
movss   dword ptr [rsp+2Ch],xmm3
divss   xmm1,xmm0
movss   dword ptr [rsp+28h],xmm1
divss   xmm2,xmm0
movss   dword ptr [rsp+2Ch],xmm2
mov     rax,qword ptr [rsp+28h]

et après ~ 30 exécutions (plus sur ce nombre plus tard), ce serait le code:

vmovsd  xmm0,qword ptr [rsp+70h]
vmovsd  qword ptr [rsp+48h],xmm0
vmovsd  xmm0,qword ptr [rsp+48h]
vmovsd  xmm1,qword ptr [rsp+48h]
vdpps   xmm0,xmm0,xmm1,0F1h
vsqrtss xmm0,xmm0,xmm0
vinsertps xmm0,xmm0,xmm0,0Eh
vshufps xmm0,xmm0,xmm0,50h
vmovsd  qword ptr [rsp+40h],xmm0
vmovsd  xmm0,qword ptr [rsp+48h]
vmovsd  xmm1,qword ptr [rsp+40h]
vdivps  xmm0,xmm0,xmm1
vpslldq xmm0,xmm0,8
vpsrldq xmm0,xmm0,8
vmovq   rcx,xmm0

Différents opcodes, différentes extensions - SSE vs AVX et, je suppose, avec différents opcodes, nous obtenons une précision différente des calculs.

Alors maintenant, plus sur le pourquoi? .NET Core (pas sûr de la version - en supposant 3.0 - mais il a été testé en 2.1) a quelque chose qui s'appelle "compilation JIT à plusieurs niveaux". Ce qu'il fait, c'est qu'au début, il produit du code qui est généré rapidement, mais qui n'est peut-être pas super optimal. Ce n'est que plus tard, lorsque le runtime détecte que le code est très utilisé, qu'il passe du temps supplémentaire à générer du nouveau code, plus optimisé. Il s'agit d'une nouveauté dans .NET Core, de sorte qu'un tel comportement pourrait ne pas être observé plus tôt.

Aussi pourquoi 34 appels? C'est un peu étrange car je m'attendrais à ce que cela se produise autour de 30 exécutions car c'est le seuil auquel la compilation à plusieurs niveaux entre en jeu. La constante peut être vue dans le code source de coreclr . Il y a peut-être une certaine variabilité supplémentaire au moment où il entre en jeu.

Juste pour confirmer que c'est le cas, vous pouvez désactiver la compilation à plusieurs niveaux en définissant la variable d'environnement en émettant set COMPlus_TieredCompilation=0et en vérifiant à nouveau l'exécution. L'effet étrange a disparu.

C:\Users\lukas\source\repos\FloatMultiple\FloatMultiple\bin\Release\netcoreapp3.1
λ FloatMultiple.exe

0000: <0,9750545  -0,22196561>
0001: <0,9750545  -0,22196561>
0002: <0,9750545  -0,22196561>
...
0032: <0,9750545  -0,22196561>
0033: <0,9750545  -0,22196561>
0034: <0,9750545  -0,22196561>
0035: <0,97505456  -0,22196563>
0036: <0,97505456  -0,22196563>
^C
C:\Users\lukas\source\repos\FloatMultiple\FloatMultiple\bin\Release\netcoreapp3.1
λ set COMPlus_TieredCompilation=0

C:\Users\lukas\source\repos\FloatMultiple\FloatMultiple\bin\Release\netcoreapp3.1
λ FloatMultiple.exe

0000: <0,97505456  -0,22196563>
0001: <0,97505456  -0,22196563>
0002: <0,97505456  -0,22196563>
...
0032: <0,97505456  -0,22196563>
0033: <0,97505456  -0,22196563>
0034: <0,97505456  -0,22196563>
0035: <0,97505456  -0,22196563>
0036: <0,97505456  -0,22196563>

Est-ce prévu ou s'agit-il d'un bogue dans la langue / le runtime?

Un bogue a déjà été signalé à ce sujet - Problème 1119

Paweł Łukasik
la source
Ils n'ont aucune idée de ce qui en est la cause. J'espère que le PO peut suivre et publier un lien vers votre réponse ici.
Hans Passant
1
Merci pour la réponse complète et informative! Ce rapport de bogue est en fait mon rapport que j'ai déposé après avoir posté cette question, sans savoir s'il s'agissait vraiment d'un bogue ou non. On dirait qu'ils considèrent la valeur changeante comme un comportement indésirable qui pourrait entraîner des bugs heisen et quelque chose qui devrait être corrigé.
Walt D
Ouais, j'aurais dû vérifier le dépôt avant de faire l'analyse à 2 heures du matin :) Quoi qu'il en soit, c'était un problème intéressant à examiner.
Paweł Łukasik
@HansPassant Désolé, je ne suis pas sûr de ce que vous proposez de faire. Pouvez-vous clarifier s'il vous plait?
Walt D
Ce problème de github a été posté par vous, n'est-ce pas? Faites-leur simplement savoir qu'ils ont mal compris.
Hans Passant