Dlaczego wynik Vector2.Normalize () zmienia się po wywołaniu go 34 razy z identycznymi danymi wejściowymi?

10

Oto prosty program C # .NET Core 3.1, który wywołuje System.Numerics.Vector2.Normalize()w pętli (z identycznym wejściem dla każdego wywołania) i drukuje wynikowy znormalizowany wektor:

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

A oto wynik działania tego programu na moim komputerze (obciętym dla zwięzłości):

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

Więc moje pytanie brzmi: dlaczego wynik połączenia Vector2.Normalize(v)zmienia się z <0.9750545, -0.22196561>na <0.97505456, -0.22196563>po wywołaniu go 34 razy? Czy jest to oczekiwane, czy jest to błąd w języku / środowisku wykonawczym?

Walt D.
źródło
Pływaki są dziwne
Milney
2
@Milney Może, ale są też deterministyczne . To zachowanie nie jest wyjaśnione wyłącznie dziwnymi pływakami.
Konrad Rudolph

Odpowiedzi:

14

Więc moje pytanie brzmi: dlaczego wynik wywołania Vector2.Normalize (v) zmienia się z <0,9750545, -0,22196561> na <0,97505456, -0,22196563> po wywołaniu go 34 razy?

Po pierwsze - dlaczego taka zmiana występuje. Zmieniono to, ponieważ zmienia się również kod obliczający te wartości.

Jeśli włamiemy się do WinDbg wcześnie w pierwszych wykonaniach kodu i przejdziemy nieco do kodu, który oblicza Normalizewektor ed, możemy zobaczyć następujący zestaw (mniej więcej - wyciąłem niektóre części):

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]

a po ~ 30 wykonaniach (więcej o tym numerze później) będzie to kod:

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

Różne kody, różne rozszerzenia - SSE vs AVX i, jak sądzę, przy różnych kodach otrzymujemy inną precyzję obliczeń.

Więc teraz więcej o tym, dlaczego? .NET Core (nie jestem pewien co do wersji - przy założeniu 3.0 - ale został przetestowany w wersji 2.1) ma coś, co nazywa się „kompilacją warstwowego JIT”. Na początku tworzy kod, który jest generowany szybko, ale może nie być superoptymalny. Dopiero później, gdy środowisko wykonawcze wykryje, że kod jest wysoce wykorzystywany, poświęci trochę czasu na wygenerowanie nowego, bardziej zoptymalizowanego kodu. Jest to nowa rzecz w .NET Core, więc takie zachowanie może nie zostać zaobserwowane wcześniej.

Także dlaczego 34 połączenia? Jest to trochę dziwne, ponieważ spodziewałbym się, że tak się stanie około 30 wykonań, ponieważ jest to próg, przy którym rozpoczyna się kompilacja warstwowa. Stałą można zobaczyć w kodzie źródłowym coreclr . Może jest jakaś dodatkowa zmienność w stosunku do momentu uruchomienia.

Aby potwierdzić, że tak jest, możesz wyłączyć kompilację warstwową, ustawiając zmienną środowiskową, set COMPlus_TieredCompilation=0ponownie wydając i sprawdzając wykonanie. Dziwny efekt zniknął.

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>

Czy jest to oczekiwane, czy jest to błąd w języku / środowisku wykonawczym?

Zgłoszono już błąd dotyczący tego problemu - numer 1119

Paweł Łukasik
źródło
Nie mają pojęcia, co go powoduje. Mamy nadzieję, że OP może śledzić i opublikować link do Twojej odpowiedzi tutaj.
Hans Passant
1
Dzięki za dokładną i pouczającą odpowiedź! Ten raport o błędzie jest w rzeczywistości moim raportem, który złożyłem po opublikowaniu tego pytania, nie wiedząc, czy to naprawdę błąd, czy nie. Wygląda na to, że uważają zmienną wartość za niepożądane zachowanie, które może spowodować błędy w działaniu i coś, co powinno zostać naprawione.
Walt D
Tak, powinienem był sprawdzić repo przed wykonaniem analizy o 2 nad ranem :) W każdym razie interesujący problem stanowił problem.
Paweł Łukasik
@HansPassant Przepraszamy, nie jestem pewien, co sugerujesz, żebym zrobił. Czy możesz wyjaśnić?
Walt D
Ten problem z githubem został opublikowany przez ciebie, prawda? Po prostu daj im znać, że źle zgadli.
Hans Passant