Warum ändert sich das Ergebnis von Vector2.Normalize (), nachdem es 34 Mal mit identischen Eingaben aufgerufen wurde?

10

Hier ist ein einfaches C # .NET Core 3.1-Programm, das System.Numerics.Vector2.Normalize()in einer Schleife (mit identischer Eingabe bei jedem Aufruf) aufruft und den resultierenden normalisierten Vektor ausgibt:

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}");
        }
    }
}

Und hier ist die Ausgabe des Ausführens dieses Programms auf meinem Computer (der Kürze halber abgeschnitten):

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

Meine Frage ist also, warum sich das Ergebnis des Aufrufs Vector2.Normalize(v)von <0.9750545, -0.22196561>nach <0.97505456, -0.22196563>nach 34-maligem Aufruf von auf ändert . Wird dies erwartet oder ist dies ein Fehler in der Sprache / Laufzeit?

Walt D.
quelle
Schwimmer sind komisch
Milney
2
@Milney Vielleicht, aber sie sind auch deterministisch . Dieses Verhalten wird nicht nur dadurch erklärt, dass Floats seltsam sind.
Konrad Rudolph

Antworten:

14

Meine Frage ist also, warum sich das Ergebnis des Aufrufs von Vector2.Normalize (v) nach 34 Aufrufen von <0.9750545, -0.22196561> auf <0.97505456, -0.22196563> ändert.

Also zuerst - warum die Änderung auftritt. Die Änderung wird beobachtet, weil sich auch der Code ändert, der diese Werte berechnet.

Wenn wir früh in den ersten Ausführungen des Codes in WinDbg einbrechen und ein wenig in den Code eintauchen, der den Normalizeed-Vektor berechnet , können wir die folgende Assembly sehen (mehr oder weniger - ich habe einige Teile gekürzt):

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]

und nach ~ 30 Hinrichtungen (mehr zu dieser Nummer später) wäre dies der 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

Unterschiedliche Opcodes, unterschiedliche Erweiterungen - SSE vs AVX und ich denke, mit unterschiedlichen Opcodes erhalten wir unterschiedliche Genauigkeit der Berechnungen.

Also jetzt mehr über das Warum? .NET Core (nicht sicher über die Version - vorausgesetzt 3.0 - aber es wurde in 2.1 getestet) hat etwas, das als "Tiered JIT Compilation" bezeichnet wird. Am Anfang wird Code generiert, der schnell generiert wird, aber möglicherweise nicht optimal ist. Erst später, wenn die Laufzeit erkennt, dass der Code stark ausgelastet ist, wird zusätzliche Zeit aufgewendet, um neuen, optimierten Code zu generieren. Dies ist eine neue Sache in .NET Core, sodass ein solches Verhalten möglicherweise nicht früher beobachtet wird.

Auch warum 34 Anrufe? Dies ist etwas seltsam, da ich davon ausgehen würde, dass dies bei etwa 30 Ausführungen geschieht, da dies der Schwellenwert ist, ab dem die gestufte Kompilierung einsetzt. Die Konstante ist im Quellcode von coreclr zu sehen . Vielleicht gibt es eine zusätzliche Variabilität, wenn es losgeht.

Um zu bestätigen, dass dies der Fall ist, können Sie die gestufte Kompilierung deaktivieren, indem Sie die Umgebungsvariable festlegen, set COMPlus_TieredCompilation=0indem Sie die Ausführung erneut ausgeben und überprüfen. Der seltsame Effekt ist weg.

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>

Wird dies erwartet oder ist dies ein Fehler in der Sprache / Laufzeit?

Hierfür wurde bereits ein Fehler gemeldet - Ausgabe 1119

Paweł Łukasik
quelle
Sie haben keine Ahnung, was es verursacht. Hoffentlich kann das OP hier einen Link zu Ihrer Antwort veröffentlichen.
Hans Passant
1
Vielen Dank für die gründliche und informative Antwort! Dieser Fehlerbericht ist eigentlich mein Bericht, den ich nach dem Posten dieser Frage eingereicht habe, ohne zu wissen, ob es sich wirklich um einen Fehler handelt oder nicht. Klingt so, als würden sie den sich ändernden Wert als unerwünschtes Verhalten betrachten, das zu Heisenbugs führen könnte und etwas, das behoben werden sollte.
Walt D
Ja, ich hätte das Repo überprüfen sollen, bevor ich die Analyse um 2 Uhr morgens durchführte :) Auf jeden Fall war es ein interessantes Problem, es zu untersuchen.
Paweł Łukasik
@ HansPassant Entschuldigung, ich bin mir nicht sicher, was Sie mir vorschlagen. Können Sie bitte klarstellen?
Walt D
Das Github-Problem wurde von Ihnen gepostet, nicht wahr? Lassen Sie sie einfach wissen, dass sie falsch geraten haben.
Hans Passant