为什么 Vector2.Normalize() 的结果在使用相同输入调用 34 次后会发生变化?

Wal*_*t D 10 .net c# .net-core

这是一个简单的 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}");
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

这是在我的计算机上运行该程序的输出(为简洁起见被截断):

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>
...
Run Code Online (Sandbox Code Playgroud)

所以我的问题是,为什么调用34次后调用的结果Vector2.Normalize(v)会从<0.9750545, -0.22196561>到变为<0.97505456, -0.22196563>?这是预期的,还是语言/运行时中的错误?

Paw*_*sik 14

所以我的问题是,为什么调用 Vector2.Normalize(v) 的结果在调用 34 次后从 <0.9750545, -0.22196561> 变为 <0.97505456, -0.22196563> ?

所以首先 - 为什么会发生变化。观察到更改是因为计算这些值的代码也发生了变化。

如果我们在代码的第一次执行中尽早进入 WinDbg 并深入研究计算Normalizeed 向量的代码,我们可以看到以下程序集(或多或少 - 我已经削减了一些部分):

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]
Run Code Online (Sandbox Code Playgroud)

在大约 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
Run Code Online (Sandbox Code Playgroud)

不同的操作码,不同的扩展——SSE 与 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>
Run Code Online (Sandbox Code Playgroud)

这是预期的,还是语言/运行时中的错误?

已经为此报告了一个错误 -问题 1119