Чому результат Vector2.Normalize () змінюється після виклику його 34 рази з однаковими входами?


10

Ось проста програма C # .NET Core 3.1, яка викликає System.Numerics.Vector2.Normalize()цикл (з однаковим входом кожного виклику) і друкує отриманий нормалізований вектор:

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

Ось результат запуску цієї програми на моєму комп’ютері (усічений для стислості):

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

Так що моє запитання, чому результат виклику Vector2.Normalize(v)переходу від <0.9750545, -0.22196561>до <0.97505456, -0.22196563>після його виклику 34 раз? Це очікується, чи це помилка в мові / час виконання?


Поплавки дивні
Мілні

2
@Milney Можливо, але вони також детерміновані . Таку поведінку не пояснюють лише дивні поплавці.
Конрад Рудольф

Відповіді:


14

Отже, моє запитання полягає в тому, чому результат виклику Vector2.Normalize (v) змінюється від <0.9750545, -0.22196561> до <0.97505456, -0.22196563> після виклику його 34 рази?

Отже, спочатку - чому відбувається зміна. Змінене спостерігається, оскільки змінюється і код, який обчислює ці значення.

Якщо ми ввірвемося в WinDbg на початку перших виконань коду і трохи підемо вниз до коду, який обчислює Normalizeвектор вектора, ми можемо побачити наступну збірку (більш-менш - я скоротив деякі частини):

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]

і після ~ 30 виконання (докладніше про це число згодом) це буде код:

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

Різні опкоди, різні розширення - SSE vs AVX і, я думаю, з різними кодами ми отримуємо різну точність обчислень.

Тож тепер докладніше про те, чому? .NET Core (не впевнений у версії - припускаючи 3.0 - але вона була протестована в 2.1) має щось, що називається "багатоярусна компіляція JIT". Що робиться, це на початку, він створює код, який генерується швидко, але може бути не надто оптимальним. Лише пізніше, коли під час виконання роботи виявиться, що код широко використовується, він витратить додатковий час для створення нового, більш оптимізованого коду. Це нова річ у .NET Core, тому така поведінка може не спостерігатися раніше.

Також чому 34 дзвінки? Це дещо дивно, тому що я б очікував, що це станеться близько 30 виконання, оскільки це поріг, при якому починається багаторівнева компіляція. Константа можна побачити у вихідному коді coreclr . Можливо, є якась додаткова мінливість, коли вона починається.

Тільки щоб підтвердити, що це так, ви можете відключити багаторівневу компіляцію, встановивши змінну навколишнього середовища, видавши set COMPlus_TieredCompilation=0та перевіряючи виконання ще раз. Дивний ефект пропав.

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>

Це очікується, чи це помилка в мові / час виконання?

Про це вже повідомляється про помилку - випуск 1119


Вони не мають поняття, що це викликає. Сподіваємось, ОП може продовжити та розмістити посилання на вашу відповідь тут.
Ганс Пасант

1
Дякуємо за ретельну та інформативну відповідь! Цей звіт про помилку - це фактично мій звіт, який я подав після публікації цього питання, не знаючи, чи це справді помилка чи ні. Схоже, що вони вважають, що зміна значення є небажаною поведінкою, що може призвести до виникнення помилок і щось, що слід виправити.
Walt D

Так, я мав би перевірити репо перед тим, як робити аналіз о 2 ранку :) Так чи інакше, це було цікаво розглянути.
Paweł Łukasik

@HansPassant Вибачте, я не впевнений, що ви пропонуєте зробити. Ви можете, будь ласка, уточнити?
Walt D

Цей номер github опублікував ви, чи не так? Просто дайте їм знати, що вони неправильно здогадалися.
Ганс Пасант
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.