Perché il risultato di Vector2.Normalize () cambia dopo averlo chiamato 34 volte con input identici?


10

Ecco un semplice programma C # .NET Core 3.1 che chiama System.Numerics.Vector2.Normalize()in un ciclo (con input identico ogni chiamata) e stampa il vettore normalizzato risultante:

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

Ed ecco l'output di eseguire quel programma sul mio computer (troncato per brevità):

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

Quindi la mia domanda è: perché il risultato della chiamata Vector2.Normalize(v)cambia da <0.9750545, -0.22196561>a <0.97505456, -0.22196563>dopo averlo chiamato 34 volte? È previsto o è un bug nella lingua / runtime?


I galleggianti sono strani
Milney il

2
@Milney Forse, ma sono anche deterministici . Questo comportamento non è spiegato unicamente dal fatto che i float sono strani.
Konrad Rudolph,

Risposte:


14

Quindi la mia domanda è: perché il risultato della chiamata Vector2.Normalize (v) cambia da <0.9750545, -0.22196561> a <0.97505456, -0.22196563> dopo averlo chiamato 34 volte?

Quindi, innanzitutto: perché si verifica il cambiamento. La modifica viene osservata perché cambia anche il codice che calcola quei valori.

Se entriamo in WinDbg nelle prime esecuzioni del codice e andiamo un po 'giù nel codice che calcola il Normalizevettore ed, potremmo vedere il seguente assembly (più o meno - ho tagliato alcune parti):

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]

e dopo ~ 30 esecuzioni (più su questo numero in seguito) questo sarebbe il codice:

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

Codici operativi diversi, estensioni diverse - SSE vs AVX e, immagino, con codici operativi diversi otteniamo una precisione dei calcoli diversa.

Quindi ora di più sul perché? .NET Core (non sono sicuro della versione - presupponendo 3.0 - ma è stato testato in 2.1) ha qualcosa che si chiama "compilazione JIT a livelli". Quello che fa è all'inizio produce codice che viene generato velocemente, ma potrebbe non essere super ottimale. Solo più tardi, quando il runtime rileva che il codice è molto utilizzato, impiegherà del tempo aggiuntivo per generare un nuovo codice più ottimizzato. Questa è una novità in .NET Core, pertanto tale comportamento potrebbe non essere osservato in precedenza.

Anche perché 34 chiamate? Questo è un po 'strano poiché mi aspetto che ciò accada intorno alle 30 esecuzioni poiché questa è la soglia alla quale entra in gioco la compilazione a livelli. La costante può essere vista nel codice sorgente di coreclr . Forse c'è qualche ulteriore variabilità rispetto a quando entra in gioco.

Solo per confermare che questo è il caso, è possibile disabilitare la compilazione a livelli impostando la variabile ambientale emettendo set COMPlus_TieredCompilation=0e controllando di nuovo l'esecuzione. Lo strano effetto è sparito.

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>

È previsto o è un bug nella lingua / runtime?

È già stato segnalato un bug per questo - Numero 1119


Non hanno idea di cosa sia la causa. Speriamo che l'OP possa dare seguito e pubblicare un link alla tua risposta qui.
Hans Passant il

1
Grazie per la risposta completa e istruttiva! Quella segnalazione di bug è in realtà la mia segnalazione che ho presentato dopo aver pubblicato questa domanda, non sapendo se fosse davvero un bug o meno. Sembra che considerino il cambiamento di valore come un comportamento indesiderato che potrebbe causare heisenbugs e qualcosa che dovrebbe essere risolto.
Walt D,

Sì, avrei dovuto controllare il repository prima di fare l'analisi alle 2:00 :) Comunque è stato un problema interessante da esaminare.
Paweł Łukasik,

@HansPassant Siamo spiacenti, non sono sicuro di cosa stai suggerendo di fare. Potete per favore chiarire?
Walt D,

Quel problema di github è stato pubblicato da te, vero? Fagli sapere che hanno indovinato.
Hans Passant il
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.