System.Math.Round 损坏

System.Math.Round corruption

提问人:Mud 提问时间:6/29/2023 最后编辑:Cole TobinMud 更新时间:7/5/2023 访问量:354

问:

我有一个系统,在运行几个小时后开始产生不正确的值。我在调试器下重现了它,发现问题是开始返回不正确的值。System.Math.Round

我有两个相同版本的 Visual Studio 实例,在同一台机器上并排运行,使用相同的项目,相同的代码,在堆栈跟踪的同一部分 - 一切都是相同的 - 除了一个已经运行了几个小时并开始失败,另一个没有。

我在它们各自的“即时”窗口中执行一个常量表达式,并得到不同的值

在良好的运行中:

enter image description here

在糟糕的运行中:

enter image description here

这个小差异对我的应用程序有重大影响。

.NET 版本,从运行的代码中转储:

System.Environment.Version=> 4.0.30319.42000

(typeof(string).Assembly.GetCustomAttributes(typeof(AssemblyFileVersionAttribute), false))[0]=> 4.8.4644.0

以前有人见过这个吗?这是一个已知的错误吗?有没有办法解决它?


编辑:@Kit不信任即时窗口,所以这里有更多信息。我显示了“即时窗口”结果,因为它可以让您看到相同的常量表达式从 产生不同的结果。下面是实际代码中与之相关的行,您可以看到在实际代码中也产生了错误的值:Math.RoundMath.Round

enter image description here

C# .NET 浮点

评论

14赞 Hans Passant 6/29/2023
你的进程的位数很重要,我猜是 32 位(又名 x86)。在这种情况下,Round() 由 FRNDINT fpu 指令实现。其行为受 FPU 控制寄存器中选择的舍入模式的影响。使用“调试”> Windows >寄存器,右键单击该工具窗口并勾选“浮点”。.NET 程序必须始终使用 CTRL = 027F 进行操作。逐步完成您的程序,当您看到它发生变化时,您就找到了邪恶代码。重置控制寄存器的一种简单方法是故意抛出异常并捕获它。
5赞 Mud 6/29/2023
@HansPassant太棒了!好的例子有,坏的例子有!您知道是什么原因会导致舍入模式发生变化吗?“单步执行代码”以找到它的变化位置是站不住脚的。它有数万行代码,遍历数万条记录,并对每条记录执行相同的操作,只有当它运行数小时(或者可能命中一个特定的不良记录)时,我们才会突然进入这种状态。顺便说一句,我修改了错误实例中的值以导致异常。被抓住并处理后,我仍然有 CTRL = 067F。CTRL = 027FCTRL = 067F
7赞 Hans Passant 6/29/2023
是的,067F 意味着它从“四舍五入到最近”变为“四舍五入”。绝对是原因。我什么都没看就帮你找到它。怀疑调用本机代码的库。此处介绍了如何调用 _controlfp() 来恢复控制字。
8赞 Flydog57 6/29/2023
@HansPassant:你应该总结一下你的建议和 Mud 发现的答案。这将是一个该死的好 SO 问题,有了这个答案。
5赞 Mud 6/29/2023
@Tudeschizieuinchid这就是为什么在没有复制案例的情况下提出问题是合理的。在这种情况下,这几乎是不可能的,但我有一个非常孤立且非常不寻常的症状,我想把它放在专家的眼前。一位这样的专家确定了根本原因并解决了我的问题。如果这个问题和答案在我开始时就在这里,我会节省几天时间。这就是这个网站的用途。

答:

13赞 Mud 6/30/2023 #1

@HansPassant在评论中发现了问题:

Hans:“你的进程的位数很重要,我猜是 32 位(又名 x86)。在这种情况下,Round() 由 FRNDINT fpu 指令实现。其行为受 FPU 控制寄存器中选择的舍入模式的影响。使用“调试”> Windows >寄存器,右键单击该工具窗口并勾选“浮点”。.NET 程序必须始终使用 CTRL = 027F 进行操作。逐步完成你的程序,当你看到它发生变化时,你就找到了邪恶的代码。

这是完全正确的。这是一个 32 位 .NET 应用程序,这显然意味着它使用 FPU 而不是 SSE 指令。这些是好实例与坏实例中的浮点寄存器:

enter image description here

enter image description here

汉斯:“067F的意思是从'四舍五入'变成了'四舍五入'。

此代码库在停用之前只需要成功运行一次,因此我没有尝试查找哪个非托管依赖项正在更改此标志以及何时更改此标志。相反,我只是在我的应用程序中添加了这样的东西,并在执行重要工作之前调用它:

    [DllImport("msvcrt.dll")]
    private static extern int _controlfp(int IN_New, int IN_Mask);

    public static void VerifyFpuRoundingMode()
    {
        const int _MCW_RC  = 0x00000300;
        const int _RC_NEAR = 0x00000000; 
        int ctrl = _controlfp(0, 0);
        if ((ctrl & _MCW_RC) != 0)
        {
            _controlfp(_RC_NEAR, _MCW_RC);
        }
    }

这解决了我们的舍入问题。