Почему результат 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
@ Милни Возможно, но они также детерминированы . Это поведение не объясняется исключительно странностью поплавков.
Конрад Рудольф

Ответы:

14

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

Итак, во-первых, почему происходит изменение. Изменения наблюдаются, потому что код, который вычисляет эти значения, тоже изменяется.

Если мы начнем с WinDbg в начале выполнения первого кода и немного углубимся в код, который вычисляет Normalizeвектор ed, мы можем увидеть следующую сборку (более или менее - я сократил некоторые части):

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
Спасибо за исчерпывающий и информативный ответ! Этот отчет об ошибке на самом деле является моим отчетом, который я подал после публикации этого вопроса, не зная, действительно ли это была ошибка или нет. Похоже, они считают, что изменяющееся значение является нежелательным поведением, которое может привести к появлению heisenbugs и тому, что следует исправить.
Уолт Д.
Да, я должен был проверить репо, прежде чем делать анализ в 2 часа ночи :) В любом случае, это была интересная проблема.
Павел Лукасик
@HansPassant Извините, я не уверен, что вы предлагаете мне сделать. Можете ли вы уточнить?
Уолт Д
Этот вопрос GitHub был опубликован вами, не так ли? Просто дайте им знать, что они догадались неправильно.
Ганс