在C#中浮点math是否一致? 是真的吗?

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

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

对于存储重播的video游戏来说,这是一个问题,或者是对等networking (而不是服务器 – 客户端),这依赖于所有客户端在每次运行程序时产生完全相同的结果 – 一个小的差异浮点计算可能会导致不同机器上的游戏状态(甚至是在同一台机器上 )

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

我已经看到了这个问题的几个解决scheme在线,但所有的C + +,而不是C#:

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

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

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

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

我知道无法在.net中使正常的浮点确定性。 JITter被允许创build在不同平台(或不同版本的.net之间)上行为不同的代码。 所以在确定性的.net代码中使用正常的float是不可能的。

我考虑的解决方法:

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

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

C#规范(§4.1.6浮点types)特别允许使用高于结果的精度完成浮点计算。 所以,不,我不认为你可以直接在.Net中确定这些计算。 其他人提出了各种解决方法,所以你可以尝试一下。

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

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

关于定点的说明

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

  • 加法和减法是微不足道的。 他们的工作方式与整数相同。 只需加或减!
  • 要乘以两个定点数,将这两个数相乘,然后向右移动定义的小数位数。
  • 除以两个定点数,移动被除数的小数位后,除以除数。
  • 本文的第四章对实现二进制定点数有更多的指导。

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

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

 // 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]; 

这是C#的问题吗?

是。 不同的体系结构是你最担心的问题,不同的帧率等可能会导致由于浮点表示中的不准确而导致的偏差 – 即使它们具有相同的不准确性(例如,同一架构,除了一台机器上较慢的GPU)。

我可以使用System.Decimal吗?

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

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

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

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

从来没听说过。

还有另一种解决方法吗?

我之前解决过这个问题,想法是使用QNumbers 。 它们是固定点的一种forms, 但不是基于10(十进制)的固定点 – 而是基2(二进制); 正因为如此,他们的math基元(add,sub,mul,div)比天真的base-10固定点要快得多。 特别是如果n是相同的两个值(在你的情况下,这将是)。 此外,因为它们是不可或缺的,所以在每个平台上都有明确的结果。

请记住,framerate仍然可以影响这些,但它并不坏,很容易使用同步点来纠正。

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

是的,往返十进制来做到这一点。 此外,你真的应该使用查找表 trig(sin,cos)函数; 因为那些在不同的平台上可以给出不同的结果 – 如果你正确的编码,他们可以直接使用QNumbers。

根据这个稍微老的MSDN博客条目 ,JIT将不会使用SSE / SSE2作为浮点,这全是x87。 正因如此,正如你所提到的,你不得不担心模式和标志,而在C#中这是不可能控制的。 所以使用正常的浮点运算不能保证你的程序在每台机器上都有完全相同的结果。

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

根据您所需要的操作,您可能能够以单一的精度脱身。 这是这个想法:

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

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

应该在这个scheme中工作的操作:加法,减法,乘法,除法,sqrt。 像罪,exp等事情将无法正常工作(结果通常会匹配,但没有保证)。 “什么时候双杀无伤大雅?” ACM参考(付费注册请求)

希望这可以帮助!

正如其他答案所述:是的,这是C#中的问题 – 即使保持纯Windows。

至于解决scheme:如果使用内置的BigInteger类,并通过使用公分母来计算/存储这样的数字,则可以减less(并且用一些努力/性能命中)来完全避免该问题。

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

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

BigInteger “解决scheme”做了类似的事情 – 只有你可以定义你需要/想要多less数字…也许你只需要80位或240位的精度。

缓慢来源于不必使用CPU / FPU内置指令而必须通过整数指令来模拟这些数字的所有操作,这些指令反过来导致每个math操作的更多指令。

为了减less性能的影响,有几个策略 – 如QNumbers(请参阅Jonathan Dickinson的回答 – 在C#中浮点math是否一致?是否可以? )和/或caching(例如触发计算等)。

那么,这将是我第一次尝试如何做到这一点

  1. 创build一个ATL.dll项目,该项目中包含一个简单的对象,用于您的关键浮点操作。 确保使用禁止使用任何非xx87硬件做浮点的标志进行编译。
  2. 创build调用浮点运算并返回结果的函数; 从简单的开始,如果它为你工作,你可以随时增加复杂性,以满足后续的性能需求。
  3. 把control_fp调用放在实际的math上,以确保它在所有机器上都以相同的方式完成。
  4. 参考你的新库和testing,以确保它按预期工作。

(我相信你可以编译成一个32位的.dll文件,然后将它与x86或AnyCpu一起使用(或者可能只在64位系统上针对x86;请参阅下面的注释)。

然后,假设它可行,如果你想使用单声道,我想你应该能够以类似的方式在其他x86平台上复制图书馆(当然不是COM;虽然,也许,与葡萄酒?有一点不在我的区域我们去那里虽然…)。

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

我不是一个游戏开发者,虽然我在计算困难的问题上有很多经验,所以我会尽我所能。

我会采取的策略本质上是这样的:

  • 使用较慢的(如果有必要;如果有更快的方式,太棒了!),但可预测的方法来获得可重现的结果
  • 使用双重的一切(例如,渲染)

短暂的这个是:你需要find一个平衡点。 如果你花费30ms渲染(〜33fps),只有1ms进行碰撞检测(或者插入一些其他高度敏感的操作) – 即使你花了三倍的时间来做关键算术,它对你的帧率的影响是你从33.3fps下降到30.3fps。

我build议你分析一切,考虑花费多less时间来做每一个明显昂贵的计算,然后用一个或多个解决这个问题的方法重复测量,看看有什么影响。

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

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

你肯定知道在任何浮动操作之后CPU会做一些调整。 和CPU提供了几个不同的指令,使不同的舍入操作。

所以对于一个expression式,你的编译器会select一组指令来引导你的结果。 但是任何其他指令工作stream程,即使他们打算计算相同的expression式,也可以提供另一个结果。

四舍五入调整所产生的“错误”将在每一个进一步的指令中增长。

作为一个例子,我们可以说在汇编层面:a * b * c不等于a * c * b。

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

但是,要回答你的问题:在C或C ++中,你可以解决你的问题,因为你有一些控制你的编译器生成的机器代码,但是在.NET中你没有。 所以只要你的机器代码可以不同,你永远不会确定的确切结果。

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

我希望我已经清楚了,我的英语不完美(… …)