System.Math.Round corruption

huangapple go评论68阅读模式
英文:

System.Math.Round corruption

问题

I have a system that starts producing incorrect values after running for several hours. I reproduced it while under a debugger and found that the problem is System.Math.Round begins returning incorrect values.

I have two instances of the same version of Visual Studio, running side by side on the same machine, with the same project, the same code, at the same part of the stack trace -- everything is identical -- except one has been running for hours and has begun failing, the other hasn't.

I execute a constant expression in their respective Immediate windows, and get different values.

In the good run:

System.Math.Round corruption

In the bad run:

System.Math.Round corruption

This small discrepancy has significant implications for my app.

.NET version, dumped from the running code:

System.Environment.Version => 4.0.30319.42000

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

Has anyone seen this before? Is it a known bug? Is there a way I can workaround it?


EDIT: @Kit doesn't trust the Immediate Window so here's a bit more info. I showed the Immediate Window result because it lets you see that the same constant expression is producing different results from Math.Round. Below is the line in the actual code where it's relevant, and you can see that Math.Round is producing the wrong value in the actual code, too:

System.Math.Round corruption

英文:

I have a system that starts producing incorrect values after running for several hours. I reproduced it while under a debugger and found that the problem is System.Math.Round begins returning incorrect values.

I have two instances of the same version of Visual Studio, running side by side on the same machine, with the same project, the same code, at the same part of the stack trace -- everything is identical -- except one has been running for hours and has begun failing, the other hasn't.

I execute a constant expression in their respective Immediate windows, and get different values.

In the good run:

System.Math.Round corruption

In the bad run:

System.Math.Round corruption

This small discrepancy has significant implications for my app.

.NET version, dumped from the running code:

System.Environment.Version => 4.0.30319.42000

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

Has anyone seen this before? Is it a known bug? Is there a way I can workaround it?


EDIT: @Kit doesn't trust the Immediate Window so here's a bit more info. I showed the Immediate Window result because it lets you see that the same constant expression is producing different results from Math.Round. Below is the line in the actual code where it's relevant, and you can see that Math.Round is producing the wrong value in the actual code, too:

System.Math.Round corruption

答案1

得分: 13

@HansPassant 在评论中识别出了问题:

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

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

Hans:“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);
    }
}

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

英文:

@HansPassant identified the problem in the comments:

> Hans: "The bitness of your process matters a lot, I'll guess at 32-bit (aka x86). In which case Round() is implemented by the FRNDINT fpu instruction. Whose behavior is affected by the rounding mode selected in the FPU control register. Use Debug > Windows > Registers, right-click that tool window and tick "Floating point". A .NET program must always be operating with CTRL = 027F. Step through your program and when you see it change then you found the Evil Code."

This is exactly right. It's a 32 bit .NET app, which apparently means it uses the FPU rather than SSE instructions. These were the floating point registers in the good instance vs the bad:

System.Math.Round corruption

System.Math.Round corruption

> Hans: "067F means it changed from 'round to nearest' to 'round down'."

This code base only needed to run successfully once before being retired, so I didn't try to find which unmanaged dependency was changing this flag and when. Instead, I just added something like this to my app and called it before doing important bits of work:

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

This fixed our rounding issues.

huangapple
  • 本文由 发表于 2023年6月29日 01:04:48
  • 转载请务必保留本文链接:https://go.coder-hub.com/76575321.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定