关于.NET:浮点数学在C中是否一致?可以吗?

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

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

最近我读了很多关于浮点的文章,特别是,相同的计算如何在不同的架构或优化设置上产生不同的结果。

这是存储重播的视频游戏的一个问题,或者是对等网络(与服务器客户端相反),它依赖于所有客户端每次运行程序时都会生成完全相同的结果-一个浮点计算中的微小差异可能会导致不同计算机(甚至是同一台机器!)

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

我已经在网上看到了几个解决这个问题的方法,但都是C++,而不是C语言:

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

所以,这是C中的一个问题吗?如果我只想支持Windows(而不是Mono)呢?

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

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


我不知道如何使.NET中的普通浮点具有确定性。允许抖动创建在不同平台(或.NET的不同版本)上行为不同的代码。因此,在确定性.NET代码中使用普通的float是不可能的。

我考虑的解决方法是:

  • 在C_中实现FixedPoint32。虽然这并不太难(我有一个完成了一半的实现),但是值的范围很小,使用起来很麻烦。你必须时刻小心,这样你既不会溢出,也不会失去太多的精度。最后,我发现这并不比直接使用整数容易。
  • 在C_中实现FixedPoint64。我觉得这很难做到。对于某些操作,128位的中间整数是有用的。但是.NET不提供这样的类型。
  • 实现自定义32位浮点。在实现这一点时,缺乏位扫描反转的内在特性会导致一些麻烦。但目前我认为这是最有希望的途径。
  • 使用本机代码进行数学运算。在每次数学运算中都会产生委托调用的开销。
  • 我刚刚开始了32位浮点数学的软件实现。它可以在我的2.66GHz i3上每秒做大约7000万次加法/乘法。https://github.com/codesinchaos/softfloat.很明显,它仍然很不完整,而且很麻烦。


    C规范(§4.1.6浮点类型)专门允许使用高于结果精度的精度进行浮点计算。所以,不,我认为你不能直接在.NET中使这些计算具有确定性。其他人建议了各种各样的解决办法,所以你可以试试。


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

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

    关于定点的注记

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

    • 加减法很简单。它们的工作方式与整数相同。只需加或减!
    • 要使两个定点数字相乘,请将两个数字相乘,然后向右移动定义的小数位数。
    • 要将两个定点数相除,请将被除数左移指定的小数位数,然后除以除数。
    • 本文第四章对二元不动点数的实现作了进一步的指导。

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

    正如在另一个答案中建议的那样,您可以使用查找表,其中表中的每个元素都是一个二进制定点数字,以帮助实现复杂的函数,如正弦、余弦、平方根等等。如果查找表的粒度小于固定点号,建议将查找表粒度的一半添加到输入中,对输入进行四舍五入:

    1
    2
    3
    4
    5
    6
    7
    8
    // 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吗?

    你没有理由不能,不过这是狗慢。

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

    对。自己调用CLR运行库;在调用CorBindToRuntimeEx之前,将所有的NICE调用/标志(改变浮点运算的行为)编译成C++应用程序。

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

    我不知道。

    还有别的方法可以解决这个问题吗?

    我以前解决过这个问题,我的想法是使用Qnumbers。它们是固定点的real形式;但不是以10为基数(十进制)的固定点,而不是以2为基数(二进制)的固定点;因此,它们上面的数学原语(add、sub、mul、div)比简单的10为基数的固定点快得多;特别是如果两个值的n相同(在您的情况下是这样)。此外,因为它们是整体的,所以在每个平台上都有明确的结果。

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

    我可以用更多的数学函数来表示Q数吗?

    是的,往返一个十进制数。此外,您真的应该为trig(sin,cos)函数使用查找表;因为它们可以在不同的平台上给出不同的结果——如果您正确地编码它们,它们可以直接使用qnumber。


    根据这个稍微旧一点的msdn博客条目,jit不会使用sse/sse2作为浮点,它都是x87。因此,正如您所提到的,您必须担心模式和标志,而在C中,这是不可能控制的。因此,使用正常的浮点运算并不能保证在您的程序中的每台机器上都有完全相同的结果。

    要获得双精度的精确再现性,您需要进行软件浮点(或定点)仿真。我不知道C图书馆会这么做。

    根据您需要的操作,您可能能够以单精度逃脱。这里的想法是:

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

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

    应在此方案中工作的操作:加法、减法、乘法、除法、sqrt。像sin、exp等的东西是行不通的(结果通常是匹配的,但没有保证)。什么时候双料倒圆是无害的?"ACM参考(付费注册Req)

    希望这有帮助!


    如其他答案所述:是的,这是C中的一个问题,即使保持纯窗口。

    对于解决方案:如果您使用内置的BigInteger类,并通过使用公分母对这些数字的任何计算/存储将所有计算扩展到定义的精度,则可以完全减少(并通过一些努力/性能影响)该问题。

    根据运营商的要求-关于性能:

    System.Decimal表示一个数字,符号为1位,整数为96位,"小数位数"(表示小数点的位置)。对于所有的计算,它必须在这个数据结构上运行,不能使用任何内置在CPU中的浮点指令。

    BigInteger的"解决方案"做了一些类似的事情——只是你可以定义你需要/想要多少数字…也许您只需要80位或240位的精度。

    慢度总是来自于在不使用CPU/FPU内置指令的情况下,通过仅整数指令模拟这些数字上的所有操作,而这反过来又会导致每个数学操作产生更多的指令。

    为了减少性能冲击,有几种策略-如Qnumbers(见Jonathan Dickinson的答案)-浮点数学在C中是否一致?可以吗?)和/或缓存(例如trig calculations…)等。


    我不是一个游戏开发者,虽然我在计算难题方面有很多经验…所以,我会尽力的。

    我将采用的策略基本上是:

    • 使用较慢的(如果需要;如果有更快的方法,很好!)可预测的方法,以获得可重复的结果
    • 其他一切都使用double(例如渲染)

    这其中的长短是:你需要找到一个平衡点。如果你花费30毫秒的渲染时间(~33fps),而只有1毫秒的时间进行碰撞检测(或插入一些其他高度敏感的操作),即使你花了三倍的时间来完成关键的算术运算,它对你的帧速率的影响也会从33.3fps下降到30.3fps。

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


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

  • 创建一个atl.dll项目,该项目中有一个简单对象,可用于关键的浮点操作。确保使用禁止使用任何非xx87硬件执行浮点运算的标志编译它。
  • 创建调用浮点运算并返回结果的函数;从简单的开始,然后如果它对您有效,您可以随时增加复杂性,以满足以后的性能需求(如果需要)。
  • 把控制调用放在实际的数学周围,以确保在所有机器上都以相同的方式进行。
  • 参考您的新库并进行测试,以确保它按预期工作。
  • (我相信您可以编译为32位.dll,然后将其与x86或anycpu一起使用[或者可能仅针对64位系统上的x86;请参阅下面的注释]。)

    那么,假设它有效,您是否应该使用mono?我想您应该能够以类似的方式在其他x86平台上复制库(当然不是com;尽管,也许是葡萄酒?不过,一旦我们去了那里,就有点不在我的区域了……)。

    假设您可以使其工作,您应该能够建立自定义函数,可以同时进行多个操作来修复任何性能问题,并且您将具有浮点数学,允许您在跨平台上具有一致的结果,其中以C++编写的代码量最少,并且将其余代码保留在C++中。


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


    你的问题很难,技术性很强。不过我可能有个主意。

    您肯定知道CPU在任何浮动操作之后都会做一些调整。CPU提供了几种不同的指令,使得取整操作不同。

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

    四舍五入调整所犯的"错误"将在每一个进一步的说明中增加。

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

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

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

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

    我希望我已经足够清楚了,我的英语不太好(…一点也不好)