¿Por qué cambia el resultado de Vector2.Normalize () después de llamarlo 34 veces con entradas idénticas?

10

Aquí hay un programa simple de C # .NET Core 3.1 que llama System.Numerics.Vector2.Normalize()en un bucle (con entrada idéntica en cada llamada) e imprime el vector normalizado resultante:

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

Y aquí está el resultado de ejecutar ese programa en mi computadora (truncado por brevedad):

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

Entonces mi pregunta es, ¿por qué el resultado de llamar Vector2.Normalize(v)cambia de <0.9750545, -0.22196561>a <0.97505456, -0.22196563>después de llamarlo 34 veces? ¿Es esto esperado o es un error en el lenguaje / tiempo de ejecución?

Walt D
fuente
Las
carrozas
2
@Milney Quizás, pero también son deterministas . Este comportamiento no se explica únicamente por flotadores que son raros.
Konrad Rudolph el

Respuestas:

14

Entonces mi pregunta es, ¿por qué el resultado de llamar a Vector2.Normalize (v) cambia de <0.9750545, -0.22196561> a <0.97505456, -0.22196563> después de llamarlo 34 veces?

Primero, por qué ocurre el cambio. El cambio se observa porque el código que calcula esos valores también cambia.

Si entramos en WinDbg al principio de las primeras ejecuciones del código y profundizamos un poco en el código que calcula el Normalizevector ed, podríamos ver el siguiente ensamblaje (más o menos, he cortado algunas partes):

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]

y después de ~ 30 ejecuciones (más sobre este número más adelante) este sería el código:

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

Diferentes códigos de operación, diferentes extensiones: SSE vs AVX y, supongo, con diferentes códigos de operación obtenemos una precisión diferente de los cálculos.

¿Entonces ahora más sobre el por qué? .NET Core (no estoy seguro de la versión, suponiendo 3.0, pero se probó en 2.1) tiene algo que se llama "compilación JIT por niveles". Lo que hace es que al principio produce código que se genera rápidamente, pero que podría no ser súper óptimo. Solo más tarde, cuando el tiempo de ejecución detecte que el código es altamente utilizado, pasará un tiempo adicional para generar un código nuevo y más optimizado. Esto es algo nuevo en .NET Core, por lo que tal comportamiento podría no haberse observado antes.

¿También por qué 34 llamadas? Esto es un poco extraño, ya que esperaría que esto suceda alrededor de 30 ejecuciones, ya que este es el umbral en el que entra en juego la compilación por niveles. La constante se puede ver en el código fuente de coreclr . Tal vez hay alguna variabilidad adicional cuando se activa.

Solo para confirmar que este es el caso, puede deshabilitar la compilación escalonada configurando la variable de entorno emitiendo set COMPlus_TieredCompilation=0y comprobando la ejecución nuevamente. El extraño efecto se ha ido.

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>

¿Es esto esperado o es un error en el lenguaje / tiempo de ejecución?

Ya hay un error reportado para esto - Problema 1119

Paweł Łukasik
fuente
No tienen idea de lo que lo causa. Esperemos que el OP pueda seguir y publicar un enlace a su respuesta aquí.
Hans Passant
1
¡Gracias por la respuesta exhaustiva e informativa! Ese informe de error es en realidad mi informe que presenté después de publicar esta pregunta, sin saber si realmente era un error o no. Parece que consideran que el valor cambiante es un comportamiento no deseado que podría dar lugar a errores y algo que debería solucionarse.
Walt D
Sí, debería haber revisado el repositorio antes de hacer el análisis a las 2 AM :) De todos modos, fue un problema interesante para investigar.
Paweł Łukasik
@HansPassant Lo siento, no estoy seguro de lo que estás sugiriendo que haga. ¿Puedes por favor aclarar?
Walt D
Ese problema de Github fue publicado por usted, ¿no? Solo hágales saber que adivinaron mal.
Hans Passant el