C#中的浮点数学是否一致?是真的吗?


155

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

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

对于存储重放或对等网络(而不是服务器-客户端)的视频游戏来说,这是一个问题,它们依赖于所有客户端在每次运行程序时都产生完全相同的结果-一个小小的差异浮点计算可能导致不同机器(甚至同一台机器上!)的游戏状态发生极大的变化

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

我在线上已经看到了针对此问题的几种解决方案,但是所有解决方案都是针对C ++而不是C#:

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

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

如果是,是否有任何方法可以强制我的程序以正常的双精度运行?

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


我已经看过这个问题了,但是每个答案要么没有解决就重复这个问题,要么说“忽略它”,这不是一个选择。我在gamedev上了类似的问题,但是(由于观众的原因)大多数答案似乎都针对C ++。
BlueRaja-Danny Pflughoeft

1
这不是一个答案,但是我敢肯定,在大多数领域中,您都可以按照确定所有共享状态都是确定性的方式来设计系统,并且不会因此而导致性能显着下降
driushkin 2011年

1
@Peter您知道.net有任何快速浮点仿真吗?
CodesInChaos 2011年

1
Java是否受此问题困扰?
乔什(Josh)

3
@Josh:Java具有strictfp关键字,它强制所有计算以指定的大小(floatdouble)而不是扩展的大小进行。但是,Java在IEE-754支持方面仍然存在许多问题。很少(非常非常)的编程语言很好地支持IEE-754。
2011年

Answers:


52

我不知道如何在.net中确定正常的浮点。允许JITter创建在不同平台上(或.net不同版本之间)行为不同的代码。因此float,不可能在确定的.net代码中使用normal 。

我考虑的解决方法:

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

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


2
有一个“无限”大小的整数可用BigInteger,尽管不如本地int或它长,所以.NET确实提供了这种类型(我相信是为F#创建的,但可以在C#中使用)
符文FS

另一个选择是.NET的GNU MP包装器。它是GNU多精度库的包装,该库支持“无限”精度整数,有理数(分数)和浮点数。
科尔·约翰逊,

2
如果您要执行这些操作中的任何一个,您最好decimal先尝试一下,因为它要简单得多。只有当手头的任务太慢时,其他方法才值得考虑。
罗曼·斯塔科夫

我了解了一种特殊情况,其中浮点是确定性的。我得到的解释是:对于乘法/除法,如果FP编号之一是2的幂(2 ^ x),则有效/尾数在计算过程中不会改变。只有指数会改变(点会移动)。因此舍入将永远不会发生。结果将是确定的。
之字形

示例:2 ^ 32之类的数字表示为(指数:32,尾数:1)。如果我们将此乘以另一个浮点数(exp,man),则结果为(exp + 32,man * 1)。对于除法,结果为(expo-32,man * 1)。尾数乘以1不会改变尾数,因此它有多少位无关紧要。
之字形

28

C#规范(第4.1.6节浮点类型)特别允许使用比结果更高的精度来进行浮点计算。因此,不,我认为您无法直接在.Net中进行确定性的计算。其他人建议了各种解决方法,因此您可以尝试一下。


9
我只是意识到,如果有人分发编译后的程序集,C#规范并不重要。仅在需要源兼容性时才重要。真正重要的是CLR规范。但是我很确定它的保证与C#保证一样弱。
CodesInChaos 2011年

难道不是double在一次操作之后每次都剥离掉不需要的位,从而产生一致的结果吗?
IllidanS4希望莫妮卡回到

2
@ IllidanS4我认为这不能保证结果一致。
svick '16

14

在需要绝对移植此类操作的情况下,下一页可能会很有用。它讨论了用于测试IEEE 754标准实现的软件,包括用于仿真浮点运算的软件。但是,大多数信息可能特定于C或C ++。

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

关于固定点的注释

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

  • 加法和减法是微不足道的。它们的工作方式与整数相同。只需加或减即可!
  • 要乘以两个定点数,请将两个数字相乘,然后向右移定义数量的小数位。
  • 要除以两个固定点数,请将除数左移定义的小数位数,然后除以除数。
  • 第四章本文对执行二进制定点数更多的指导。

二进制定点数可以在任何整数数据类型(例如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];

5
您应该将其上传到开源代码项目站点,例如sourceforge或github。这使得更容易查找,更容易贡献,更容易放在履历表上。等等。此外,还有一些源代码提示(可随时忽略):使用const而不是static常量,以便编译器可以优化它们;可以使用常量代替常量。比静态函数更喜欢成员函数(因此我们可以调用,myDouble.LeadingZeros()而不是IntDouble.LeadingZeros(myDouble));尽量避免使用单字母变量名(MultiplyAnyLength例如,有9个,因此很难遵循)
BlueRaja-Danny Pflughoeft11年

出于速度目的,请小心使用unchecked和不兼容CLS的类型(例如)ulonguint因为它们很少使用,JIT并没有积极地优化它们,因此使用它们实际上比使用正常类型(例如和)要。同样,C#具有运算符重载,该项目将大大受益于此。最后,是否有任何关联的单元测试?除了这些小东西,出色的工作Peter,这真是令人难以置信longint
BlueRaja-Danny Pflughoeft

谢谢你的意见。我确实对代码执行单元测试。但是,它们的范围非常广泛,目前还无法发布。我什至编写了单元测试助手例程,以简化编写多个测试的过程。我现在不使用重载运算符,因为我计划在完成后将代码转换为Java。
Peter O.

2
有趣的是,当我在您的博客上发布时,我没有注意到该博客是您的。我刚刚决定尝试google +,并在其C#火花中建议该博客条目。所以我想“对于我们两个人同时开始写这样的东西真是太奇妙了”。但是我们当然有相同的触发条件:)
CodesInChaos 2011年

1
为什么要将此移植到Java?Java已经通过保证了确定性浮点数学运算strictfp
锑2013年

9

这是C#的问题吗?

是。最小的麻烦就是不同的体系结构,不同的帧速率等都可能由于浮点表示的不准确性而导致偏差-即使它们是相同的不准确性(例如,相同的体系结构,但一台计算机上的GPU较慢)。

我可以使用System.Decimal吗?

没有理由你不能,但是这太慢了。

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

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

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

从来没听说过。

还有另一种解决方法吗?

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

请记住,帧率仍然可以影响这些帧率,但帧率并没有那么差,并且可以使用同步化点轻松纠正。

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

是的,将小数点四舍五入即可。此外,您实际上应该在trig(sin,cos)函数中使用查找表。因为它们确实可以在不同平台上提供不同的结果-如果您对它们进行正确的编码,它们可以直接使用QNumbers。


3
不知道您在谈论帧速率问题。显然,您希望有一个固定的更新速率(例如,请参见此处)-是否与显示帧速率相同无关紧要。只要所有机器上的错误相同,我们就很好。我根本听不懂你的第三个答案。
BlueRaja-Danny Pflughoeft 2011年

@BlueRaja:答案“有没有办法让我的程序以双精度运行?” 如用户shelleybutterfly的回答所暗示的那样,要么等于重新实现整个公共语言运行时(这将极其复杂),要么使用从C#应用程序对C ++ DLL的本地调用。正如我的回答所暗示的那样,将“ QNumbers”仅视为二进制定点数(直到现在我还没有看到二进制定点数被称为“ QNumbers”。)
Peter

@PieterO。您不需要重新实现运行时。我在公司工作的服务器将CLR运行时作为本机C ++应用程序托管(SQL Server也是如此)。我建议你谷歌CorBindToRuntimeEx。
乔纳森·迪金森

@BlueRaja取决于相关游戏。对所有游戏应用固定的帧速率步长不是可行的选择-因为AOE算法会引入人为延迟。这在FPS中是不可接受的。
乔纳森·迪金森

1
@Jonathan:这只是在仅发送输入的点对点游戏中的一个问题-对于这些游戏,您必须具有固定的更新速率。大多数FPS不能像这样工作,但是少数确实必须具有固定的更新速率。看到这个问题
BlueRaja-Danny Pflughoeft

6

根据这个稍微古老的MSDN博客条目,JIT不会将SSE / SSE2用于浮点运算,它们全都是x87。因此,正如您提到的,您必须担心模式和标志,而在C#中则无法控制。因此,使用正常的浮点运算将无法保证程序中每台计算机上的结果完全相同。

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

根据您需要的操作,您也许可以单精度摆脱困境。这是主意:

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

x87的最大问题是,取决于精度标志以及寄存器是否溢出到内存,计算可能以53位或64位精度进行。但是对于许多运算,以较高的精度执行运算并舍入到较低的精度将保证正确的答案,这意味着在所有系统上都将保证答案是相同的。无论您是否获得额外的精度都无关紧要,因为您有足够的精度来保证在两种情况下的正确答案。

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

希望这可以帮助!


2
.NET 5或6或42可能不再使用x87计算模式也是一个问题。标准中没有任何要求。
埃里克·J。

5

正如其他答案已经指出的那样:是的,即使在使用纯Windows时,这也是C#中的问题。

作为一种解决方案:如果您使用内置BigInteger类并通过使用公共分母来进行此类数字的任何计算/存储,则可以完全减少该问题(并为此付出了一些努力/性能),从而完全避免了该问题。

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

System.Decimal代表带有1位符号和96位Integer和一个“小数位数”(表示小数点所在的位置)的数字。对于所有计算,您必须使其必须在此数据结构上运行,并且不能使用CPU内置的任何浮点指令。

BigInteger“办法”做类似的东西-只是你可以定义你有多少位需要/想......也许你只想要80位或精度240位。

速度缓慢总是来自必须通过仅整数指令而不使用CPU / FPU内置指令对这些数字进行所有运算的模拟,从而导致每个数学运算产生更多指令。

为了减少性能下降,有几种策略-例如QNumbers(请参阅Jonathan Dickinson的答案-C#中的浮点数学是否一致?可以吗?)和/或缓存(例如trig计算...)等。


1
请注意,BigInteger仅在.Net 4.0中可用。
svick

我的猜测是,性能BigInteger损失甚至超过了十进制的性能损失。
CodesInChaos

在这里的答案中有几次提到了使用Decimal(@Jonathan Dickinson-'dog slow')或BigInteger(@@ CodeInChaos上面的评论)的性能影响-有人可以对这些性能影响以及是否是否提供一些解释吗? /为什么他们确实是提供解决方案的阻碍者。
巴里·凯

@Yahia-谢谢您的编辑-有趣的阅读,但是,您能不能说说不使用'float'会对性能造成的影响呢?是我们说话慢了10%还是慢了10倍-我只是想要感受隐含的数量级。
巴里·凯

它在1:5的区域比“仅10%”的区域更喜欢
Yahia

2

好吧,这是我第一次尝试这样做

  1. 创建一个ATL.dll项目,该项目中包含一个简单的对象,可用于您的关键浮点操作。确保使用禁止使用任何非xx87硬件进行浮点运算的标志对其进行编译。
  2. 创建调用浮点运算并返回结果的函数;从简单开始,然后为您工作,如果需要的话,您总是可以增加复杂性以满足以后的性能需求。
  3. 将control_fp调用放在实际数学上,以确保在所有计算机上都完成相同的操作。
  4. 引用您的新库并进行测试以确保其可以正常工作。

(我相信您可以将其编译为32位.dll,然后将其与x86或AnyCpu一起使用[或可能仅针对64位系统上的x86;请参见下面的评论]。)

然后,假设它可行,您是否应该使用Mono我以为您应该能够以类似的方式在其他x86平台上复制该库(当然不是COM;虽然也许是用wine?我们去那里...)。

假设您可以使其正常运行,那么您应该能够设置自定义函数,该函数可以一次执行多个操作来解决任何性能问题,并且您将拥有浮点数学运算,可以使您在各个平台上以最小的数量获得一致的结果用C ++编写的代码集,其余的代码留在C#中。


“编译为32位.dll,然后使用... AnyCpu”,我认为这仅在32位系统上运行时有效。在64位系统上,只有目标程序x86才能加载32位dll。
CodesInChaos 2011年

2

我不是游戏开发人员,尽管我在计算难题上确实有很多经验……所以,我会尽力而为。

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

  • 使用较慢的速度(如有必要;如果有较快的方法,那就太好了!),但是可预测的方法可获得可重复的结果
  • 将double用作其他所有内容(例如,渲染)

它的短处是:您需要找到一个平衡点。如果您花费30毫秒的渲染时间(〜33 fps)并且仅用1毫秒的时间进行碰撞检测(或插入其他一些高度敏感的操作),即使您将执行关键算法所需的时间增加了三倍,它对帧速率的影响也是您从33.3fps下降到30.3fps。

我建议您对所有内容进行概要分析,并考虑花费多少时间进行每个明显昂贵的计算,然后使用一种或多种解决此问题的方法重复进行测量,并查看其影响。


1

检查其他答案中的链接可以清楚地表明,您永远无法保证浮点是否“正确”实现,或者对于给定的计算是否始终会获得一定的精度,但是也许您可以通过以下方式尽最大努力: (1)将所有计算都截断到一个共同的最小值(例如,如果不同的实现将给您32至80位的精度,则总是将每个运算都截断为30或31位),(2)在启动时有一些测试用例的表(加,减,乘,除,平方根,余弦等的边界情况),并且如果实现计算出与表匹配的值,则无需进行任何调整。


总是将每个操作截断为30或31位 -这恰恰是float数据类型在x86机器上所做的-但是,这将导致与仅使用32位进行所有计算的机器产生略有不同的结果,并且这些细小的变化将随着时间的流逝而传播。因此,这个问题。
BlueRaja-Danny Pflughoeft

如果“精度的N位”表示任何计算都可以精确到那么多位,并且机器A可以精确到32位,而机器B可以精确到48位,则两台机器的任何计算的前32位都应该相同。每次操作后都不会截断到32位或更少吗?如果没有,那是什么例子?
见证保护ID 44583292 2011年

-3

您在非常困难和技术性的问题上提出的问题O_o。但是我可能有一个主意。

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

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

四舍五入调整所产生的“错误”将在以后的进一步说明中增加。

例如,我们可以说在汇编级别:a * b * c不等于a * c * b。

我不太确定,您需要找一个比我了解CPU架构更多的人:p

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

我很好奇这可能是个问题,因为变化似乎很小,但是如果您需要真正准确的操作,我可以考虑的唯一解决方案就是增加浮动寄存器的大小。如果可以,请使用双精度或什至长双精度(不确定使用CLI可以做到)。

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


9
想象一下P2P射击游戏。你向一个男人开枪,打他,他死了,但是距离很近,你几乎错过了。在另一个人的PC上,使用略有不同的计算,并且计算出您错过的计算。您现在看到问题了吗?在这种情况下,增加寄存器的大小将无济于事(至少不是完全如此)。将在每台计算机上使用完全相同的计算。
svick

5
在这种情况下,通常不关心结果与实际结果的接近程度(只要合理),但重要的是对于所有用户而言,结果都是完全相同的。
CodesInChaos 2011年

1
是的,我不是关于这种情况。但是我同意@CodeInChaos在这一点上。我发现两次做出重要决定并不明智。这更多是软件体系结构问题。一个程序,即射手的应用程序,应进行计算并将结果发送给其他程序。这样您将永远不会出错。您是否受到打击,但只有一个人决定。就像说@driushkin
AxFab 2011年

2
@埃斯加尔:是的,多数射击者就是这样工作的。“权威”被称为服务器,而我们将整体架构称为“客户端/服务器”架构。但是,还有另一种体系结构:点对点。在P2P中,没有服务器。相反,所有客户端必须在发生任何事情之前相互验证所有操作。这会增加延迟,使射击者无法接受,但会大大减少网络流量,非常适合可接受较小延迟(〜250ms)但不能同步整个游戏状态的游戏。即,诸如C&C和Starcraft之类的RTS游戏都使用P2P。
BlueRaja-Danny Pflughoeft 2011年

5
在p2p游戏中,您没有可信赖的机器来依赖。如果您允许一个电台决定他的子弹是否被击中,则可能会导致客户作弊。此外,链接甚至无法处理有时会产生的数据量-游戏通过发送订单而不是结果来工作。我玩RTS游戏,而且很多次我都看到有很多垃圾在飞来飞去,不可能通过普通的家庭上行链路发送。
罗伦·佩希特尔
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.