Por que o resultado de Vector2.Normalize () muda depois de chamá-lo 34 vezes com entradas idênticas?

10

Aqui está um programa C # .NET Core 3.1 simples que chama System.Numerics.Vector2.Normalize()um loop (com entrada idêntica a cada chamada) e imprime o vetor 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}");
        }
    }
}

E aqui está a saída da execução desse programa no meu computador (truncado por questões de brevidade):

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

Então, minha pergunta é: por que o resultado da chamada Vector2.Normalize(v)muda de <0.9750545, -0.22196561>para <0.97505456, -0.22196563>depois de 34 vezes? Isso é esperado ou é um bug no idioma / tempo de execução?

Walt D
fonte
Flutuadores são estranhos
Milney
2
@Milney Talvez, mas eles também são determinísticos . Esse comportamento não é explicado apenas por carros alegóricos serem estranhos.
Konrad Rudolph

Respostas:

14

Então, minha pergunta é: por que o resultado da chamada Vector2.Normalize (v) muda de <0.9750545, -0.22196561> para <0.97505456, -0.22196563> após chamá-lo 34 vezes?

Então, primeiro - por que a mudança ocorre. A alteração é observada porque o código que calcula esses valores também muda.

Se entrarmos no WinDbg logo no início das primeiras execuções do código e entrarmos um pouco no código que calcula o Normalizevetor ed, poderemos ver o seguinte assembly (mais ou menos - cortei algumas 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]

e após ~ 30 execuções (mais sobre esse número mais tarde), este seria o 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

Opcodes diferentes, extensões diferentes - SSE vs AVX e, eu acho, com opcodes diferentes, obtemos uma precisão diferente dos cálculos.

Então agora mais sobre o porquê? O .NET Core (não tem certeza da versão - assumindo a versão 3.0 - mas foi testada na 2.1) possui algo chamado "compilação em camadas do JIT". O que faz é no início produzir código que é gerado rapidamente, mas pode não ser super ideal. Somente mais tarde, quando o tempo de execução detectar que o código é altamente utilizado, ele gastará algum tempo adicional para gerar um código novo e mais otimizado. Isso é algo novo no .NET Core, portanto esse comportamento pode não ser observado anteriormente.

Também por que 34 chamadas? Isso é um pouco estranho, pois eu esperaria que isso acontecesse em torno de 30 execuções, pois esse é o limite no qual a compilação em camadas entra em ação . A constante pode ser vista no código-fonte do coreclr . Talvez haja alguma variabilidade adicional para quando ele entra em ação.

Apenas para confirmar que esse é o caso, você pode desativar a compilação em camadas configurando a variável ambiental emitindo set COMPlus_TieredCompilation=0e verificando a execução novamente. O efeito estranho se foi.

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>

Isso é esperado ou é um bug no idioma / tempo de execução?

Já existe um bug relatado para isso - Edição 1119

Paweł Łukasik
fonte
Eles não têm idéia do que causa isso. Espero que o OP possa acompanhar e postar um link para sua resposta aqui.
Hans Passant
11
Obrigado pela resposta completa e informativa! Esse relatório de bug é, na verdade, o meu relatório que eu arquivei depois de postar esta pergunta, sem saber se era realmente um bug ou não. Parece que eles consideram o valor alterado um comportamento indesejável que pode resultar em erros e algo que deve ser corrigido.
Walt D
Sim, eu deveria ter verificado o repo antes de fazer a análise às 2 da manhã :) De qualquer forma, foi um problema interessante de se analisar.
Paweł Łukasik
@HansPassant Desculpe, não tenho certeza do que você está sugerindo que eu faça. Você pode esclarecer por favor?
Walt D
Esse problema no github foi postado por você, não foi? Apenas deixe-os saber que eles adivinharam errado.
Hans Passant