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

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

我最近读了很多关于浮点数的文章,特别是关于不同架构或优化设置上的 同样的计算可能得出不同的结果

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

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

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

  • 使用 _controlfp_s(Windows)、 _FPU_SETCW(Linux?)或 fpsetprec(BSD)禁用双扩展精度模式(以便所有 double计算都使用 IEEE-75464位)。
  • 始终使用相同的优化设置运行相同的编译器,并要求所有用户具有相同的 CPU 架构(不允许跨平台使用)。因为我的“编译器”实际上是 JIT,也就是 每次运行程序时可能会有不同的优化,我认为这是不可能的。
  • 使用定点,避免使用 floatdoubledecimal可以满足这个目的,但是速度要慢得多,而且 System.Math库函数都不支持它。

那么,如果我只想支持 Windows (而不是 Mono)呢?

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

如果没有,有没有图书馆可以帮助保持浮点计算的一致性?

17111 次浏览

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

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

关于定点的注记

二进制不动点数也可以很好地替代浮点数,从四种基本算术运算中可以看出:

  • 加法和减法是微不足道的。它们的工作方式与整数相同。只需要加或减!
  • 要将两个定点数相乘,先将两个数相乘,然后将定义的小数位数向右移动。
  • 要除以两个不动点数,将除数移动到定义的小数位数,然后除以除数。
  • Hattangady (2007)的第四章对实现二进制不动点数有额外的指导(S.K. Hattangady,“ 基于 DSP 的块浮点间隔 ALU 的研制及其控制应用”,硕士论文,北卡罗来纳州立大学,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];

你的问题在相当困难和技术性的东西 O _ o。然而,我可能有一个想法。

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

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

舍入调整所造成的“错误”会随着每次进一步的指示而增加。

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

我对此并不完全确定,您需要找一个比我更了解 CPU 体系结构的人: p

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

我很好奇这是一个什么样的问题,因为变化似乎非常小,但如果你真的需要准确的操作,我能想到的唯一解决方案将是增加您的浮动寄存器的大小。如果可以,使用 double 精度或者甚至长 double (不确定是否可以使用 CLI)。

我希望我已经说得够清楚了,我的英语一点也不完美

我知道没有办法让正常的浮点数在。网。允许 JITter 创建在不同平台(或不同版本的。网)。因此在确定性中使用正常的 float。网络代码是不可能的。

我考虑的变通方法是:

  1. 在 C # 中实现 FixedPoint32。虽然这并不太难(我已经完成了一半的实现) ,但是非常小的值范围使得它很难使用。你必须时刻小心,这样你既不会溢出,也不会失去太多的精度。最后,我发现这并不比直接使用整数更容易。
  2. 在 C # 中实现 FixedPoint64。我发现这很难做到。对于某些操作,128位的中间整数是有用的。但是。Net 不提供这样的类型。
  3. 实现自定义的32位浮点数。在实现此功能时,由于缺少位扫描反向内部属性会引起一些烦恼。但目前我认为这是最有希望的道路。
  4. 对数学操作使用本机代码。在每个数学操作中引起委托调用的开销。

我刚刚开始了一个32位浮点数学的软件实现。在我的2.66 GHz i3上,它每秒可以做大约7000万次的加法/乘法运算。 Https://github.com/codesinchaos/softfloat 显然还是非常不完整和漏洞百出。

C # 规范(4.1.6浮点类型)特别允许使用高于结果精度的浮点计算。所以,不,我不认为你可以直接在。网。其他人提出了各种变通方法,以便您可以尝试使用。

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

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

根据你所需要的操作,你也许可以精确地完成任务:

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

X87的一个大问题是,根据精度标志和寄存器是否溢出到内存,计算可能以53位或64位的精度进行。但是对于许多操作来说,以高精度执行操作并四舍五入到较低的精度将保证正确的答案,这意味着答案在所有系统上都是相同的。您是否得到额外的精度并不重要,因为您有足够的精度来保证两种情况下的正确答案。

在这个方案中应该工作的操作: 加法、减法、乘法、除法、 sqrt。像 sin、 exp 等这样的东西不会起作用(结果通常会匹配,但不能保证)。“什么时候双舍入是无害的?”ACM 参考文献(付费条例)

希望这个能帮上忙!

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

至于解决方案: 如果您使用内置的 BigInteger类,并且通过对这些数字的任何计算/存储使用公分母,将所有计算缩放到一个定义的精度,那么您可以完全避免这个问题(并且通过一定的努力/性能影响)。

根据业务方案的要求,在业绩方面:

System.Decimal表示一个符号为1位、96位整数和一个“刻度”(表示小数点的位置)的数字。对于所有的计算,必须对这个数据结构进行操作,不能使用内置在 CPU 中的任何浮点指令。

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

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

为了减少对性能的影响,有几种策略——比如 QNumbers (参见乔纳森·迪金森-浮点数学在 C # 中是一致的吗?的答案)和/或 cache (例如 trig 计算...)等等。

这是我对 怎么做的第一次尝试:

  1. 创建一个 ATL.dll 项目,其中包含一个用于关键浮点操作的简单对象。确保使用禁用使用任何非 xx87硬件做浮点运算的标志来编译它。
  2. 创建调用浮点操作的函数并返回结果; 从简单的开始,如果它对您有用,您总是可以增加复杂性以满足以后的性能需求(如果需要的话)。
  3. 将 control _ fp 调用放在实际的计算周围,以确保在所有计算机上以相同的方式执行。
  4. 参考您的新库并进行测试,以确保它能够按预期工作。

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

然后,假设它可以工作,那么您是否应该使用 Mono,我想您应该能够以类似的方式在其他 x86平台上复制这个库(当然不是 COM; 不过,也许可以用葡萄酒复制它?不过一旦我们去了那里,就有点超出我的范围了... ...)。

假设你可以做到这一点,你应该能够建立自定义函数,可以一次执行多个操作来修复任何性能问题,你将有浮点数学,允许你有一致的结果跨平台,最少量的代码写在 C + + ,并留下你的代码在 C # 。

这对 C # 来说是个问题吗?

是的。不同的架构是你最不需要担心的,不同的帧率等可能会因为浮点表示的不准确而导致偏差——即使它们是 一样的不准确(例如,相同的架构,除了一台机器上的 GPU 较慢)。

我可以使用系统。十进制?

没有理由你不能,不过它的狗慢。

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

是的。在调用 CorBindToRuntimeEx 之前,将所有必要的调用/标志(改变浮点运算行为的调用/标志)编译到 C + + 应用程序中。

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

据我所知没有。

还有别的办法吗?

我以前解决过这个问题,我的想法是使用 数字。它们是实数的一种形式,是固定点; 但不是以10为基数(十进制)的固定点,而是以2为基数(二进制)的固定点; 因此,它们上面的数学原语(add,sub,mul,div)要比幼稚的以10为基数的固定点快得多; 特别是如果 n对两个值都是相同的(在你的例子中就是这样)。此外,因为它们是整体的,它们在每个平台上都有定义良好的结果。

请记住,帧率仍然可以影响这些,但它没有那么糟糕,很容易纠正使用同步点。

我可以使用更多的数学函数与 QNumbers?

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

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

我不是一个游戏开发者,虽然我有很多经验与计算困难的问题... 所以,我会尽我最大的努力。

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

  • 使用较慢的(如果有必要; 如果有更快的方法,很好!) ,但可预测的方法来获得可重复的结果
  • 在其他任何事情上使用双精度(例如渲染)

简单来说就是: 你需要找到一个平衡点。如果你的渲染时间是30ms (~ 33fps) ,而碰撞侦测只有1ms (或者插入一些其他高度敏感的操作)——即使你的渲染时间是关键算术的三倍,它对你帧率的影响也是从33.3 fps 降到30.3 fps。

我建议你分析每一件事,说明每一次显著昂贵的计算花费了多少时间,然后用一种或多种方法重复测量来解决这个问题,看看影响是什么。