我一直在阅读有关.NET中浮点确定性的内容,即确保相同的代码和输入在不同的计算机上给出相同的结果。由于.NET缺乏像Java的fpstrict和MSVC的fp:strict等选项,共识似乎是使用纯托管代码没有办法解决这个问题。C#游戏AI Wars已经采用定点数学作为解决方案,但这是一个繁琐的解决方案。
主要问题似乎是CLR允许中间结果存储在比类型本地精度更高的FPU寄存器中,从而导致无法预测的更高的精度结果。CLR工程师David Notario在MSDN文章中解释了以下内容:
引用: 注意,按照当前规范,仍然是语言选择要给出“可预测性”。语言可以在每个FP操作后插入conv.r4或conv.r8指令以获得“可预测”的行为。显然,这非常昂贵,不同的语言有不同的折衷方案。例如,C#什么也不做,如果需要缩小,则必须手动插入(float)和(double)转换。
这表明,只需为每个求值为浮点数的表达式和子表达式插入显式转换,就可以实现浮点确定性。可以编写一个包装类型来自动化此任务。这将是一个简单而理想的解决方案!
然而,其他评论表明情况并非如此简单。Eric Lippert最近表示(重点是我的):
引用: 在某个版本的运行时中,显式强制转换为float会产生与不这样做不同的结果。当您显式转换为float时,C#编译器向运行时提供提示,以表示“拿走这个东西。”如果你恰好正在使用这种优化技术,那么你需要退出额外高精度模式。这个“提示”对运行时来说是什么意思呢?C#规范是否规定了显式转换为float会在IL中插入conv.r4指令?CLR规范是否规定了conv.r4指令会导致值缩小到其本机大小?只有两者都成立,我们才能依靠显式转换提供浮点数的“可预测性”,如David Notario所解释的那样。最后,即使我们确实可以强制将所有中间结果转换为类型的本机大小,这是否足以保证在不同计算机上的可复现性,或者是否存在其他因素,如FPU/SSE运行时设置?
主要问题似乎是CLR允许中间结果存储在比类型本地精度更高的FPU寄存器中,从而导致无法预测的更高的精度结果。CLR工程师David Notario在MSDN文章中解释了以下内容:
引用: 注意,按照当前规范,仍然是语言选择要给出“可预测性”。语言可以在每个FP操作后插入conv.r4或conv.r8指令以获得“可预测”的行为。显然,这非常昂贵,不同的语言有不同的折衷方案。例如,C#什么也不做,如果需要缩小,则必须手动插入(float)和(double)转换。
这表明,只需为每个求值为浮点数的表达式和子表达式插入显式转换,就可以实现浮点确定性。可以编写一个包装类型来自动化此任务。这将是一个简单而理想的解决方案!
然而,其他评论表明情况并非如此简单。Eric Lippert最近表示(重点是我的):
引用: 在某个版本的运行时中,显式强制转换为float会产生与不这样做不同的结果。当您显式转换为float时,C#编译器向运行时提供提示,以表示“拿走这个东西。”如果你恰好正在使用这种优化技术,那么你需要退出额外高精度模式。这个“提示”对运行时来说是什么意思呢?C#规范是否规定了显式转换为float会在IL中插入conv.r4指令?CLR规范是否规定了conv.r4指令会导致值缩小到其本机大小?只有两者都成立,我们才能依靠显式转换提供浮点数的“可预测性”,如David Notario所解释的那样。最后,即使我们确实可以强制将所有中间结果转换为类型的本机大小,这是否足以保证在不同计算机上的可复现性,或者是否存在其他因素,如FPU/SSE运行时设置?
int.MinValue
除以-1
的int
除法是“实现定义的”(除非您使用checked
上下文)。引用:“[...]具体实现未指定是抛出System.ArithmeticException
(或其子类)还是溢出不报告并且结果值为左操作数的值。”所以它并不是那么“可重现”。 - Jeppe Stig Nielsenfp:strict
这样的内容不存在。再加上数值类型的通用问题(我在一周左右前曾发泄过),这似乎会使得在 C# 中进行数值分析工作变得更具挑战性。 - Ed S.