首页 文章

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

提问于
浏览
146

不,这不是另一个"Why is (1/3.0)*3 != 1"问题 .

我最近一直在读关于漂浮点的事情;具体来说, same calculation might give different results 如何在不同的架构或优化设置上 .

对于存储重放的视频游戏来说,这是一个问题,或者是peer-to-peer networked(而不是服务器 - 客户端),它依赖于每次运行程序时生成完全相同结果的所有客户端 - 一个浮点计算中的小差异可以导致不同机器上的游戏状态截然不同(甚至on the same machine!

甚至在处理器之间也会发生这种情况,主要是因为某些处理器(即x86)使用double extended precision . 也就是说,它们使用80位寄存器进行所有计算,然后截断为64位或32位,导致与使用64位或32位进行计算的机器不同的舍入结果 .

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

  • 使用_controlfp_s(Windows), _FPU_SETCW (Linux?)或fpsetprec(BSD)禁用双扩展精度模式(以便所有 double 计算使用IEEE-754 64位) .

  • 始终使用相同的优化设置运行相同的编译器,并要求所有用户具有相同的CPU架构(无跨平台播放) . 因为我的"compiler"实际上是JIT, may optimize differently every time the program is run ,我不认为这是可能的 .

  • 使用定点算术,并完全避免使用 floatdouble . decimal 可以用于此目的,但会慢得多,并且 System.Math 库函数都不支持它 .


那么, is this even a problem in C#? 如果我只打算支持Windows(不是Mono)怎么办?

如果是, is there any way to force my program to run at normal double-precision?

如果没有, are there any libraries that would help 保持浮点计算一致吗?

10 回答

  • 9

    我知道无法在.net中确定正常浮点的确定性 . 允许JITter创建在不同平台(或不同版本的.net)之间表现不同的代码 . 因此,在确定性.net代码中使用普通的 float 是不可能的 .

    我考虑过的变通办法:

    • 在C#中实现FixedPoint32 . 虽然这不是太难(我有一半完成的实现),非常小的值范围使它使用起来很烦人 . 你必须时刻小心,既不会溢出,也不会失去太多的精确度 . 最后我发现这并不比直接使用整数更容易 .

    • 在C#中实现FixedPoint64 . 我发现这很难做到 . 对于某些操作,128位的中间整数将是有用的 . 但.net不提供这样的类型 .

    • 实现自定义32位浮点 . 在实现此功能时,缺少BitScanReverse内在会导致一些烦恼 . 但目前我认为这是最有前途的途径 .

    • 使用本机代码进行数学运算 . 在每个数学运算上发生委托调用的开销 .

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

  • 6

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

  • 26

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

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

    A note on fixed point

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

    • 加法和减法是微不足道的 . 它们的工作方式与整数相同 . 只需加或减!

    • 要将两个固定点数相乘,将两个数相乘,然后向右移动定义的小数位数 .

    • 要划分两个固定点数,将被除数除以定义的小数位数,然后除以除数 .

    • this paper的第四章有关于实现二进制定点数的附加指导 .

    二进制定点数可以在任何整数数据类型上实现,例如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];
    
  • 49

    Is this a problem for C#?

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

    Can I use System.Decimal?

    没有理由你不能,但它的狗很慢 .

    Is there a way to force my program to run in double precision?

    是 . Host the CLR runtime yourself;并在调用CorBindToRuntimeEx之前将所有nessecary调用/标志(将浮点运算的行为改变)编译到C应用程序中 .

    Are there any libraries that would help keep floating point calculations consistent?

    从来没听说过 .

    Is there another way to solve this?

    我以前解决了这个问题,想法是使用QNumbers . 它们是一种定点的实物形式;但不是基数为10(十进制)的固定点 - 而是基数为2(二进制);因此,它们上的数学基元(add,sub,mul,div)比天真的基础10固定点快得多;特别是如果 n 对于两个值都是相同的(在您的情况下它将是这样) . 此外,因为它们是完整的,所以它们在每个平台上都有明确的结果 .

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

    Can I use more mathematical functions with QNumbers?

    是的,往返十进制来做这件事 . 此外,你应该真正使用lookup tables作为trig(sin,cos)函数;因为那些可以在不同的平台上给出不同的结果 - 如果你正确编码它们就可以直接使用QNumbers .

  • 4

    根据这个略显陈旧的MSDN blog entry,JIT不会使用SSE / SSE2作为浮点数,因此无法控制's all x87. Because of that, as you mentioned you have to worry about modes and flags, and in C# that' . 因此,使用正常的浮点运算并不能保证程序的每台机器都能得到完全相同的结果 .

    要获得双精度的精确再现性,您将不得不进行软件浮点(或定点)仿真 . 我不知道C#库这样做 .

    根据您需要的操作,您可能能够以单精度逃脱 . 这是个主意:

    • 以单精度存储您关心的所有值

    • 执行操作:

    • 将输入扩展为双精度

    • 以双精度执行操作

    • 将结果转换回单精度

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

    应该在此方案中运行的操作:加法,减法,乘法,除法,sqrt . 像sin,exp等的东西不起作用(结果通常会匹配,但不能保证) . "When is double rounding innocuous?" ACM Reference (paid reg. req.)

    希望这可以帮助!

  • 2

    正如其他答案已经说明的那样:是的,这是C#中的问题 - 即使在保持纯Windows时也是如此 .

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

    根据OP的要求 - 关于绩效:

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

    BigInteger "solution"做了类似的事情 - 只有你可以定义你需要/想要的位数......也许你只需要80位或240位的精度 .

    缓慢来自于必须通过仅整数指令模拟这些数字上的所有操作,而不使用CPU / FPU内置指令,这反过来导致每个数学运算更多的指令 .

    为了减少性能损失,有几种策略 - 比如QNumbers(参见Jonathan Dickinson的答案 - Is floating-point math consistent in C#? Can it be?)和/或缓存(例如触发计算......)等 .

  • 14

    好吧,这是我第一次尝试 how to do this

    • 创建一个ATL.dll项目,其中包含一个简单对象,用于关键的浮点运算 . 确保使用禁用任何非xx87硬件来执行浮点的标志来编译它 .

    • 创建调用浮点运算并返回结果的函数;从简单开始,然后如果它适合您,您可以随时增加复杂性,以便在必要时满足您的性能需求 .

    • 将control_fp调用放在实际的数学运算周围,以确保它在所有机器上以相同的方式完成 .

    • 引用新库并进行测试以确保其按预期工作 .

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

    然后,假设它有效,你是否想要使用Mono我想你应该能够以类似的方式在其他x86平台上复制库(当然不是COM;虽然,也许,对于葡萄酒?有点离开我的区域一次我们去那里虽然...) .

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

  • 2

    我不是游戏开发人员,虽然我在计算难度方面有很多经验......所以,我会尽我所能 .

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

    • 使用较慢的(如果需要;如果有更快的方法,那就太好了!),但是可预测的方法可以获得可重现的结果

    • 对其他一切使用double(例如,渲染)

    短暂的这个问题是:你需要找到一个 balancer 点 . 如果你花费30ms渲染(~33fps)并且只有1ms进行碰撞检测(或插入一些其他高度敏感的操作) - 即使你将关键算术所需的时间增加了三倍,它对你的帧速率的影响也是你从33.3fps下降到30.3fps .

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

  • -3

    检查其他答案中的链接可以清楚地表明,您永远无法保证浮点是否“正确”实现,或者您是否总能获得某个给定计算的精度,但也许您可以通过(1)将所有计算截断为一个共同的最小值(例如,如果不同的实现将为您提供32到80位的精度,总是将每个操作截断为30或31位),(2)在启动时有几个测试用例的表(加法,减法,乘法,除法,sqrt,余弦等的边界情况),如果实现计算与表匹配的值,则不打扰进行任何调整 .

  • 1

    你在非常困难和技术方面的问题O_o . 不过我可能有个主意 .

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

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

    每次进一步指示时,四舍五入调整所带来的“错误”将会增加 .

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

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

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

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

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

相关问题