为什么用相同的输入调用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>
...

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



2
@Milney也许,但是它们也是确定性的。不能仅通过浮点数的怪异来解释这种行为。
康拉德·鲁道夫

Answers:


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]

在执行大约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与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


他们不知道是什么原因造成的。希望OP可以跟进并在此处发布指向您答案的链接。
汉斯·帕桑

1
感谢您提供详尽而翔实的答案!该错误报告实际上是我在发布此问题后提交的报告,不知道它是否确实是错误。听起来他们确实认为更改值是有害的行为,可能会导致heisenbug和某些应解决的问题。
Walt D

是的,我应该在凌晨2点进行分析之前检查回购协议:)无论如何,这是一个有趣的问题。
帕维尔Lukasik的

@HansPassant对不起,我不确定您在建议我做什么。你能澄清一下吗?
Walt D

那个github问题是您发布的,不是吗?只是让他们知道他们猜错了。
汉斯·帕桑
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.