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?
Odpowiedzi:
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
Normalize
wektor ed, możemy zobaczyć następujący zestaw (mniej więcej - wyciąłem niektóre części):a po ~ 30 wykonaniach (więcej o tym numerze później) będzie to kod:
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=0
ponownie wydając i sprawdzając wykonanie. Dziwny efekt zniknął.Zgłoszono już błąd dotyczący tego problemu - numer 1119
źródło