浮点数学在 C# 中是否一致?可以吗?

Is floating-point math consistent in C#? Can it be?

提问人:BlueRaja - Danny Pflughoeft 提问时间:7/14/2011 最后编辑:CommunityBlueRaja - Danny Pflughoeft 更新时间:12/5/2021 访问量:18032

问:

不,这不是另一个“为什么是 (1/3.0)*3 != 1”的问题。

我最近一直在阅读有关浮点的文章;具体来说,相同的计算如何在不同的体系结构或优化设置上给出不同的结果

对于存储回放或点对点联网(而不是服务器-客户端)的视频游戏来说,这是一个问题,这些游戏依赖于所有客户端在每次运行程序时生成完全相同的结果 - 一个浮点计算中的微小差异可能导致不同机器(甚至在同一台机器上)的游戏状态截然不同!)

即使在“遵循”IEEE-754的处理器中也会发生这种情况,主要是因为某些处理器(即x86)使用双倍扩展精度。也就是说,它们使用 80 位寄存器进行所有计算,然后截断为 64 位或 32 位,从而导致与使用 64 位或 32 位进行计算的机器不同的舍入结果。

我在网上看到了这个问题的几种解决方案,但都是针对 C++,而不是 C#:

  • 使用 _controlfp_s (Windows)、(Linux?) 或 fpsetprec (BSD) 禁用双扩展精度模式(以便所有计算都使用 IEEE-754 64 位)。double_FPU_SETCW
  • 始终使用相同的优化设置运行相同的编译器,并要求所有用户具有相同的 CPU 架构(无跨平台游戏)。因为我的“编译器”实际上是 JIT,每次运行程序时它可能会以不同的方式进行优化,所以我认为这是不可能的。
  • 使用定点算术,并完全避免和。 将用于此目的,但速度会慢得多,并且没有任何库函数支持它。floatdoubledecimalSystem.Math

那么,这在 C# 中甚至是一个问题吗?如果我只打算支持 Windows(而不是 Mono)怎么办?

如果是,有没有办法强制我的程序以正常的双精度运行?

如果没有,是否有任何库可以帮助保持浮点计算的一致性?

C# .NET 浮点 精度 IEEE-754

评论

1赞 BlueRaja - Danny Pflughoeft 7/14/2011
我看过这个问题,但每个答案要么重复问题而没有解决方案,要么说“忽略它”,这不是一个选项。我在游戏开发上问了一个类似的问题,但是(因为观众)大多数答案似乎都是针对C++的。
1赞 driushkin 7/14/2011
不是答案,但我相信在大多数领域中,您可以以这样一种方式设计您的系统,即所有共享状态都是确定性的,并且不会因此而显着的性能下降
1赞 CodesInChaos 7/14/2011
您@Peter知道 .NET 的快速浮点仿真吗?
1赞 Josh 8/2/2011
Java有这个问题吗?
3赞 porges 8/2/2011
@Josh:Java 有 keyword,它强制所有计算以规定的大小(或)而不是扩展的大小完成。但是,Java 在 IEE-754 支持方面仍然存在许多问题。非常(非常非常)很少有编程语言很好地支持 IEE-754。strictfpfloatdouble

答:

16赞 Peter O. 7/14/2011 #1

如果您需要此类操作的绝对可移植性,以下页面可能会很有用。它讨论了用于测试 IEEE 754 标准实现的软件,包括用于模拟浮点运算的软件。但是,大多数信息可能特定于 C 或 C++。

http://www.math.utah.edu/~beebe/software/ieee/

关于不动点的说明

二进制定点数也可以很好地替代浮点数,这从四个基本的算术运算中可以明显看出:

  • 加法和减法是微不足道的。它们的工作方式与整数相同。只需加减!
  • 要将两个定点数相乘,请将两个数字相乘,然后向右移动定义的小数位数。
  • 要除以两个定点数,请将被除数移到定义的小数位数左边,然后除以除数。
  • Hattangady (2007) 的第四章提供了关于实现二进制定点数的额外指导(S.K. Hattangady,“Development of a Block Floating Point Interval ALU for DSP and Control Applications”,硕士论文,北卡罗来纳州立大学,2007 年)。

二进制定点数可以在任何整数数据类型(如 int、long 和 BigInteger)以及不符合 CLS 的类型 uint 和 ulong 上实现。

正如另一个答案中所建议的,您可以使用查找表(其中表中的每个元素都是一个二进制定点数)来帮助实现复杂的函数,例如正弦、余弦、平方根等。如果查找表的粒度小于定点数,建议通过将查找表粒度的一半添加到输入中来舍入输入:

// Assume each number has a 12 bit fractional part. (1/4096)
// Each entry in the lookup table corresponds to a fixed point number
//  with an 8-bit fractional part (1/256)
input+=(1<<3); // Add 2^3 for rounding purposes
input>>=4; // Shift right by 4 (to get 8-bit fractional part)
// --- clamp or restrict input here --
// Look up value.
return lookupTable[input];

评论

6赞 BlueRaja - Danny Pflughoeft 7/22/2011
您应该将其上传到开源代码项目站点,例如 sourceforge 或 github。这使得它更容易找到,更容易贡献,更容易放在你的简历上等。另外,一些源代码提示(随意忽略):Use instead 而不是 for 常量,以便编译器可以优化它们;首选成员函数而不是静态函数(因此我们可以调用,例如。 而不是 );尽量避免使用单字母变量名称(例如,有 9 个,因此很难理解)conststaticmyDouble.LeadingZeros()IntDouble.LeadingZeros(myDouble)MultiplyAnyLength
1赞 BlueRaja - Danny Pflughoeft 7/22/2011
出于速度目的,使用不符合 CLS 的类型(如 、 等)时要小心 - 因为它们很少使用,所以 JIT 不会积极地优化它们,因此使用它们实际上可能比使用普通类型(如 和)。此外,C# 具有运算符重载,该项目将从中受益匪浅。最后,是否有任何相关的单元测试?除了这些小事,了不起的工作彼得,这令人印象深刻!uncheckedulonguintlongint
0赞 Peter O. 7/22/2011
谢谢你的评论。我确实对代码进行了单元测试。不过,它们相当广泛,目前无法发布。我甚至编写了单元测试帮助程序例程,以便更轻松地编写多个测试。我现在不使用重载运算符,因为我计划在完成后将代码转换为 Java。
2赞 CodesInChaos 7/24/2011
有趣的是,当我在您的博客上发帖时,我没有注意到该博客是您的博客。我刚刚决定尝试 google+,并在其 C# 火花中建议该博客文章。所以我想,“我们俩同时开始写这样的东西真是太巧合了”。但当然,我们有相同的触发:)
2赞 Antimony 2/26/2013
为什么要把它移植到 Java 上呢?Java 已经通过 保证了确定性浮点数学运算。strictfp
-3赞 AxFab 7/14/2011 #2

你的问题在相当困难和技术性的东西O_o。但是,我可能有一个想法。

您肯定知道 CPU 在任何浮动操作后都会进行一些调整。 CPU 提供了几种不同的指令,这些指令可以进行不同的舍入操作。

因此,对于表达式,编译器将选择一组指令来引导您获得结果。但是,任何其他指令工作流,即使它们打算计算相同的表达式,也可以提供另一个结果。

舍入调整所犯的“错误”将在每次进一步的指令中增加。

举个例子,我们可以说,在汇编级别上:a * b * c 不等同于 a * c * b。

我不完全确定,你需要找一个比我更了解 CPU 架构的人:p

但是,要回答您的问题:在 C 或 C++ 中,您可以解决问题,因为您可以对编译器生成的机器代码进行一些控制,但是在 .NET 中,您没有任何控制权。因此,只要您的机器代码可能不同,您就永远无法确定确切的结果。

我很好奇这在哪种方式上可能是一个问题,因为变化似乎非常小,但如果您需要真正准确的操作,我能想到的唯一解决方案就是增加浮动寄存器的大小。如果可以的话,使用双精度甚至长双精度(不确定使用 CLI 是否可行)。

我希望我已经足够清楚了,我的英语并不完美(...全部 : s)

评论

9赞 svick 7/14/2011
想象一下P2P射击游戏。你向一个人开枪,你击中了他,他死了,但它非常接近,你差点错过了。在另一个人的 PC 上使用略有不同的计算,它会计算您错过的计算。你现在看到问题了吗?在这种情况下,增加寄存器的大小将无济于事(至少不完全)。在每台计算机上使用完全相同的计算即可。
5赞 CodesInChaos 7/14/2011
在这种情况下,人们通常不关心结果与实际结果的接近程度(只要它是合理的),但重要的是它对所有用户来说都是完全相同的。
1赞 AxFab 7/14/2011
你说得对,我没有想到这种情况。但是,我同意@CodeInChaos这一点。我不认为两次做出重要决定真的很聪明。这更像是一个软件架构问题。一个程序,即射手的示例应用程序,应该进行计算并将结果发送给其他程序。以这种方式,您永远不会出错。你命中与否,但只有一个人接受谴责。比如说@driushkin
3赞 BlueRaja - Danny Pflughoeft 7/14/2011
@Aesgar:是的,大多数射手都是这样工作的;这个“权威”被称为服务器,我们称整体架构为“客户端/服务器”架构。但是,还有另一种架构:点对点。在P2P中,没有服务器;相反,所有客户端必须在发生任何事情之前相互验证所有操作。这增加了延迟,使射击游戏无法接受,但大大减少了网络流量,使其非常适合小延迟(~250 毫秒)可以接受但无法同步整个游戏状态的游戏。也就是说,像C&C和星际争霸这样的RTS游戏使用P2P。
5赞 Loren Pechtel 7/17/2011
在 p2p 游戏中,您没有值得信赖的机器可以依赖。如果你允许一个电台决定他的子弹是否击中,你就有可能让客户作弊。此外,这些链接甚至无法处理有时产生的数据量——游戏通过发送订单而不是结果来工作。我玩RTS游戏,很多时候我看到这么多垃圾飞来飞去,不可能通过正常的家庭上行链路发送。
56赞 CodesInChaos 7/14/2011 #3

我不知道没有办法在.net中使普通浮点确定性。允许 JITter 创建在不同平台(或不同版本的 .net)上行为不同的代码。因此,在确定性的 .net 代码中使用普通 s 是不可能的。float

我考虑的解决方法:

  1. 在 C# 中实现 FixedPoint32。虽然这并不难(我有一个半成品的实现),但值的范围非常小,使用起来很烦人。你必须时刻小心,这样你既不会溢出,也不会失去太多的精度。最后,我发现这并不比直接使用整数容易。
  2. 在 C# 中实现 FixedPoint64。我发现这很难做到。对于某些操作,128 位的中间整数会很有用。但 .net 不提供这样的类型。
  3. 实现自定义 32 位浮点。缺少 BitScanReverse 内部函数会导致在实现此操作时出现一些烦恼。但目前我认为这是最有希望的道路。
  4. 使用本机代码进行数学运算。在每个数学运算上产生委托调用的开销。

我刚刚开始了 32 位浮点数学的软件实现。它在我的 2.66GHz i3 上每秒可以进行大约 7000 万次加法/乘法。https://github.com/CodesInChaos/SoftFloat .显然,它仍然非常不完整且有问题。

评论

3赞 Rune FS 12/12/2012
有一个“无限”大小的整数可用 BigInteger,虽然不如本机 int 快,或者它很长,所以 .NET 确实提供了这样的类型(我相信是为 F# 创建的,但可以在 C# 中使用)
2赞 Roman Starkov 4/15/2015
如果你打算做其中任何一个,你不妨先试试,因为它做起来要简单得多。只有当它对于手头的任务来说太慢时,其他方法才值得考虑。decimal
0赞 zigzag 9/8/2016
我了解到一种特殊情况,其中浮点是确定性的。我得到的解释是:对于乘法/除法,如果其中一个 FP 数是两个数 (2^x) 的幂,则有效/尾数在计算过程中不会改变。只有指数会改变(点会移动)。所以四舍五入永远不会发生。结果将是确定性的。
0赞 zigzag 9/8/2016
示例:像 2^32 这样的数字表示为 (exponent: 32, mantissa: 1)。 如果我们将其与另一个浮点数 (exp, man) 相乘,结果是 (exp + 32, man * 1)。对于除法,结果是(世博会 - 32,人 * 1)。将尾数乘以 1 不会改变尾数,因此它有多少位并不重要。
0赞 Thomas Padron-McCarthy 7/12/2018
对于反对票,我们深表歉意。我在手机上点击了错误(如果这是一个词),现在我无法更改它。
29赞 svick 7/14/2011 #4

C# 规范(§4.1.6 浮点类型)明确允许使用高于结果的精度进行浮点计算。所以,不,我认为你不能直接在.Net中使这些计算具有确定性。其他人提出了各种解决方法,因此您可以尝试一下。

评论

9赞 CodesInChaos 8/3/2011
我刚刚意识到,如果分发已编译的程序集,C#规范并不重要。只有当一个人想要源兼容性时,它才重要。真正重要的是 CLR 规范。但我敢肯定,它的保证和 C# 保证一样弱。
0赞 IS4 2/28/2016
难道每次操作后都进行强制转换不会去除不需要的位,从而产生一致的结果吗?double
4赞 svick 2/28/2016
@IllidanS4我认为这并不能保证一致的结果。
6赞 Nathan Whitehead 7/19/2011 #5

根据这篇稍旧的 MSDN 博客文章,JIT 不会使用 SSE/SSE2 作为浮点,它都是 x87。因此,正如您提到的,您必须担心模式和标志,而在 C# 中,这是无法控制的。因此,使用普通的浮点运算并不能保证程序在每台机器上获得完全相同的结果。

为了获得双精度的精确再现性,您必须进行软件浮点(或定点)仿真。我不知道 C# 库可以做到这一点。

根据您需要的操作,您也许能够摆脱单一精度。这个想法是这样的:

  • 以单个精度存储您关心的所有值
  • 要执行操作:
    • 将输入扩展到双精度
    • 双精度DO操作
    • 将结果转换回单精度

x87 的最大问题是计算可能以 53 位或 64 位精度完成,具体取决于精度标志以及寄存器是否溢出到内存中。但对于许多操作,以高精度执行操作并四舍五入到较低精度将保证正确答案,这意味着答案将保证在所有系统上都是相同的。你是否获得额外的精度并不重要,因为你有足够的精度来保证在任何一种情况下都能得到正确的答案。

在此方案中应该起作用的运算:加法、减法、乘法、除法、sqrt。诸如sin,exp等之类的东西是行不通的(结果通常会匹配,但不能保证)。“什么时候双舍五入是无害的?”ACM 参考(付费注册要求)

希望这有帮助!

评论

2赞 Eric J. 2/15/2012
.NET 5、6 或 42 可能不再使用 x87 计算模式也是一个问题。标准中没有任何内容要求这样做。
5赞 Yahia 7/20/2011 #6

正如其他答案已经指出的那样: 是的,这是 C# 中的一个问题 - 即使保持纯 Windows。

至于解决方案: 如果使用内置类,并通过对此类数字的任何计算/存储使用公分母将所有计算缩放到定义的精度,则可以完全避免该问题(并降低一些工作量/性能)。BigInteger

根据 OP 的要求 - 关于性能:

System.Decimal表示数字,其中 1 位表示符号,96 位整数和“小数位数”(表示小数点的位置)。对于您进行的所有计算,它必须对此数据结构进行操作,并且不能使用 CPU 中内置的任何浮点指令。

“解决方案”做了类似的事情 - 只是您可以定义您需要/想要多少位数......也许您只想要 80 位或 240 位的精度。BigInteger

缓慢总是来自必须通过纯整数指令模拟这些数字的所有操作,而不使用 CPU/FPU 内置指令,这反过来又导致每个数学运算的指令要多得多。

为了减少对性能的影响,有几种策略 - 比如 QNumbers(参见 Jonathan Dickinson 的回答 - 浮点数学在 C# 中是一致的吗?可以吗?和/或缓存(例如trig计算...)等。

评论

1赞 svick 7/21/2011
请注意,这仅在 .Net 4.0 中可用。BigInteger
0赞 CodesInChaos 7/24/2011
我的猜测是,性能影响甚至超过了 Decimal 的性能。BigInteger
0赞 Barry Kaye 8/1/2011
在这里的答案中,有几次提到了使用(@Jonathan Dickinson - “狗慢”)或(上面@CodeInChaos评论)的性能影响 - 有人可以对这些性能打击提供一些解释,以及它们是否/为什么真的是提供解决方案的障碍。DecimalBigInteger
0赞 Barry Kaye 8/2/2011
@Yahia - 感谢您的编辑 - 有趣的阅读,但是,您能否也对不使用“浮点数”的性能影响进行大致猜测,我们谈论的是慢 10% 还是慢 10 倍 - 我只是想感受一下隐含的数量级。
0赞 Yahia 8/2/2011
它更有可能在 1:5 的范围内,而不是“只有 10%”
2赞 shelleybutterfly 7/23/2011 #7

好吧,这将是我第一次尝试如何做到这一点

  1. 创建一个 ATL.dll 项目,其中包含用于关键浮点运算的简单对象。确保使用禁止使用任何非 xx87 硬件执行浮点运算的标志来编译它。
  2. 创建调用浮点运算并返回结果的函数;从简单开始,然后如果它适合您,您可以随时增加复杂性以满足以后的需求(如有必要)。
  3. 将control_fp调用放在实际的数学运算上,以确保在所有机器上都以相同的方式完成。
  4. 引用您的新库并进行测试,以确保其按预期工作。

(我相信你可以编译成一个32位的.dll,然后用x86或AnyCpu[或者可能只针对64位系统上的x86;见下面的注释]。

然后,假设它有效,如果你想使用 Mono,我想你应该能够以类似的方式在其他 x86 平台上复制该库(当然不是 COM;虽然,也许,用 wine?一旦我们去那里,就有点超出我的区域......

假设您可以使其工作,您应该能够设置可以一次执行多个操作的自定义函数来修复任何性能问题,并且您将拥有浮点数学,允许您使用最少的代码量在 C++ 中跨平台获得一致的结果,并将其余代码保留在 C# 中。

评论

0赞 CodesInChaos 7/24/2011
“编译为 32 位 .dll,然后使用 ...AnyCpu“我认为这仅在 32 位系统上运行时才有效。在 64 位系统上,只有目标程序才能加载 32 位 dll。x86
9赞 Jonathan Dickinson 8/1/2011 #8

这是 C# 的问题吗?

是的。不同的架构是您最不担心的,不同的帧速率等可能会导致由于浮点表示不准确而导致偏差 - 即使它们是相同的不准确(例如,相同的架构,除了一台机器上的 GPU 速度较慢)。

我可以使用 System.Decimal 吗?

你没有理由不能,但它很慢。

有没有办法强制我的程序以双精度运行?

是的。自行承载 CLR 运行时;并在调用 CorBindToRuntimeEx 之前将所有 nessecary 调用/标志(更改浮点运算的行为)编译到 C++ 应用程序中。

是否有任何库可以帮助保持浮点计算的一致性?

我不知道。

有没有其他方法可以解决这个问题?

我以前已经解决了这个问题,我的想法是使用 QNumbers。它们是定点实数的一种形式;但不是以 10 为基数(十进制)的固定点 - 而是以 2 为基数(二进制);正因为如此,它们上的数学基元(add、sub、mul、div)比朴素的以 10 为底的不动点要快得多;特别是如果两个值相同(在您的情况下是相同的)。此外,由于它们是不可或缺的,因此它们在每个平台上都有明确定义的结果。n

请记住,帧速率仍然会影响这些,但它并没有那么糟糕,并且很容易使用同步点进行纠正。

我可以在 QNumbers 中使用更多数学函数吗?

是的,往返小数点即可执行此操作。此外,您确实应该对 trig (sin, cos) 函数使用查找表;因为这些确实可以在不同的平台上给出不同的结果 - 如果你正确地编码它们,它们可以直接使用 QNumbers。

评论

4赞 BlueRaja - Danny Pflughoeft 8/2/2011
不确定你在说什么,帧率是问题。显然,你希望有一个固定的更新速率(参见此处的示例)——这是否与显示帧率相同无关紧要。只要所有机器上的不准确之处都相同,我们就很好。我完全不明白你的第三个答案。
0赞 Peter O. 8/2/2011
@BlueRaja:答案“有没有办法强制我的程序以双精度运行?”要么相当于重新实现整个公共语言运行库,这将非常复杂,要么使用从 C# 应用程序对 C++ DLL 的本机调用,正如用户 shelleybutterfly 的回答中所暗示的那样。正如我的回答中所暗示的那样,将“QNumbers”仅仅视为二进制定点数(直到现在我还没有看到二进制定点数被称为“QNumbers”)。
0赞 Jonathan Dickinson 8/3/2011
@Pieter O.您无需重新实现运行时。我在公司工作的服务器将 CLR 运行时作为本机 C++ 应用程序托管(SQL Server 也是如此)。我建议你谷歌 CorBindToRuntimeEx。
0赞 Jonathan Dickinson 8/3/2011
@BlueRaja这取决于所讨论的游戏。对所有游戏应用固定帧速率步长不是一个可行的选择 - 因为 AOE 算法引入了人为延迟;这在例如FPS中是不可接受的。
1赞 BlueRaja - Danny Pflughoeft 8/4/2011
@Jonathan:这只是点对点游戏中的问题,它只发送输入 - 对于这些游戏,你必须有一个固定的更新率。大多数 FPS 不是这样工作的,但少数 FPS 必须具有固定的更新率。请参阅此问题
1赞 mike 8/2/2011 #9

检查其他答案中的链接可以清楚地看出,你永远无法保证浮点是否“正确”实现,或者你是否总是能获得给定计算的一定精度,但也许你可以尽最大努力(1)将所有计算截断到一个共同的最小值(例如,如果不同的实现会给你32到80位的精度, 总是将每个操作截断为 30 或 31 位),(2) 在启动时有一个包含一些测试用例的表格(加、减、乘、除、sqrt、余弦等临界情况),如果实现计算的值与表格匹配,则无需费心进行任何调整。

评论

0赞 BlueRaja - Danny Pflughoeft 8/2/2011
总是将每个操作截断为 30 位或 31 位 - 这正是 DataType 在 x86 机器上所做的 - 但这将导致与仅使用 32 位进行所有计算的机器略有不同的结果,并且这些小的变化会随着时间的推移而传播。因此,问题来了。float
0赞 Witness Protection ID 44583292 8/3/2011
如果“N 位精度”意味着任何计算精确到这么多位,并且机器 A 精确到 32 位,而机器 B 精确到 48 位,那么两台机器的任何计算的前 32 位都应该相同。每次操作后截断到 32 位或更少,难道不会使两台机器完全同步吗?如果不是,举个例子是什么?
2赞 Brian Vandenberg 8/2/2011 #10

我不是游戏开发者,尽管我确实在计算困难的问题方面有很多经验......所以,我会尽力而为。

我采用的策略基本上是这样的:

  • 使用较慢(如有必要;如果有更快的方法,那就太好了!),但可预测的方法来获得可重复的结果
  • 对其他所有事情(例如,渲染)使用 double

简而言之:你需要找到一个平衡点。如果你花了 30 毫秒渲染 (~33fps),而只有 1 毫秒进行碰撞检测(或插入一些其他高度敏感的操作)——即使你把做关键算术所需的时间增加了三倍,它对帧率的影响是你从 33.3fps 下降到 30.3fps。

我建议你分析所有内容,说明每次计算所花费的时间,然后用一种或多种方法来解决这个问题,看看有什么影响。