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

Posted

技术标签:

【中文标题】C# 中的浮点数学是不是一致?是真的吗?【英文标题】:Is floating-point math consistent in C#? Can it be?C# 中的浮点数学是否一致?是真的吗? 【发布时间】:2011-10-04 17:06:49 【问题描述】:

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

我最近读了很多关于浮点的文章;具体来说,相同的计算如何在不同的架构或优化设置上产生不同的结果

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

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

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

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

那么,这在 C# 中是否是个问题?如果我只打算支持 Windows(而不是 Mono)怎么办?

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

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

【问题讨论】:

我见过this question,但是每个答案要么重复问题而没有解决方案,要么说“忽略它”,这不是一个选项。我问了a similar question on gamedev,但是(因为观众)大多数答案似乎都是针对C++的。 不是一个答案,但我敢肯定,在大多数领域中,您都可以将系统设计为所有共享状态都是确定性的,并且不会因此而显着降低性能 @Peter 你知道 .net 的任何快速浮点仿真吗? Java有这个问题吗? @Josh:Java 有 strictfp 关键字,它强制所有计算以规定的大小(floatdouble)而不是扩展大小进行。但是,Java 在 IEE-754 支持方面仍然存在许多问题。非常(非常、非常)少数编程语言能够很好地支持 IEE-754。 【参考方案1】:

我不知道如何在 .net 中确定正常的浮点数。 JITter 允许创建在不同平台(或不同版本的 .net)上行为不同的代码。所以在确定性 .net 代码中使用普通的floats 是不可能的。

我考虑的解决方法:

    在 C# 中实现 FixedPoint32。虽然这并不太难(我已经完成了一半的实现),但非常小的值范围使其使用起来很烦人。您必须始终小心,以免溢出,也不会丢失太多精度。最后我发现这并不比直接使用整数更容易。 在 C# 中实现 FixedPoint64。我发现这很难做到。对于某些操作,128 位的中间整数会很有用。但是 .net 不提供这种类型。 实现自定义 32 位浮点。在实现这一点时,缺少 BitScanReverse 内在函数会导致一些烦恼。但目前我认为这是最有希望的路径。 使用本机代码进行数学运算。每次数学运算都会产生委托调用的开销。

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

【讨论】:

有一个“无限”大小的整数可用 BigInteger 虽然不如本机 int 快或 long 它在那里所以 .NET 确实提供了这种类型(我相信为 F# 创建但可以在 C# 中使用) 另一个选项是GNU MP wrapper for .NET。它是The GNU Multiple Precision Library 的包装器,支持“无限”精度整数、有理数(分数)和浮点数。 如果你打算做这些,你不妨先试试decimal,因为它更简单。只有当手头的任务太慢时,其他方法才值得考虑。 我了解了一种浮点是确定性的特殊情况。我得到的解释是:对于乘法/除法,如果 FP 数之一是两个数的幂 (2^x),则在计算过程中有效/尾数不会改变。只有指数会改变(点会移动)。所以舍入永远不会发生。结果将是确定性的。 示例:像 2^32 这样的数字表示为(指数:32,尾数:1)。如果我们将它与另一个浮点数 (exp, man) 相乘,结果是 (exp + 32, man * 1)。对于除法,结果是 (expo - 32, man * 1)。尾数乘以 1 不会改变尾数,所以它有多少位并不重要。【参考方案2】:

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

【讨论】:

我刚刚意识到,如果分发已编译的程序集,C# 规范并不重要。只有想要源兼容性才重要。真正重要的是 CLR 规范。但我很确定它的保证与 C# 的保证一样弱。 不会在每次操作去除不需要的位后都转换为double,从而产生一致的结果? @IllidanS4 我认为这不能保证结果一致。【参考方案3】:

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

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

关于定点的说明

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

加法和减法是微不足道的。它们的工作方式与整数相同。只需加减即可! 要将两个定点数相乘,请将这两个数相乘,然后右移定义的小数位数。 要将两个定点数相除,请将被除数左移定义的小数位数,然后除以除数。 Hattangady (2007) 的第四章提供了关于实现二进制定点数的附加指南(S.K. Hattangady,“Development of a Block Floating Point Interval ALU for DSP and Control Applications”,硕士论文,北卡罗来纳州立大学,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];

【讨论】:

您应该将其上传到开源代码项目站点,例如 sourceforge 或 github。这使得更容易找到,更容易贡献,更容易放在你的简历等。此外,一些源代码提示(随意忽略):使用const而不是static作为常量,所以编译器可以优化它们;更喜欢成员函数而不是静态函数(所以我们可以调用,例如 myDouble.LeadingZeros() 而不是 IntDouble.LeadingZeros(myDouble));尽量避免使用单字母变量名(例如MultiplyAnyLength,有 9 个,很难理解) 谨慎使用 unchecked 和不符合 CLS 的类型,如 ulonguint 等,以提高速度 - 因为它们很少使用,JIT 不会将它们优化为积极地,因此使用它们实际上可能比使用普通类型(如longint。此外,C# 有 operator overloading,这个项目将从中受益匪浅。最后,是否有任何相关的单元测试?除了那些小事,了不起的工作彼得,这真是令人印象深刻! 感谢 cmets。我确实对代码执行单元测试。但是,它们相当广泛,目前无法发布。我什至编写了单元测试帮助程序来简化编写多个测试的过程。我暂时不使用重载运算符,因为我计划在完成后将代码转换为 Java。 有趣的是,当我在您的博客上发帖时,我没有注意到该博客是您的。我刚刚决定尝试 google+,并在其 C# spark 中建议了该博客条目。所以我想“我们两个同时开始写这样的东西真是太巧合了”。但当然我们有相同的触发器:) 为什么要把它移植到Java? Java 已经通过strictfp 保证了确定性浮点数学。【参考方案4】:

这是 C# 的问题吗?

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

我可以使用 System.Decimal 吗?

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

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

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

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

我不知道。

还有其他方法可以解决这个问题吗?

我之前解决过这个问题,想法是使用QNumbers。它们是定点实数的一种形式;但不是以 10 为底的定点(十进制) - 而是以 2 为底的(二进制);因此,它们上的数学原语(add、sub、mul、div)比简单的 base-10 固定点快得多;特别是如果n 对于两个值都相同(在您的情况下是这样)。此外,由于它们是不可或缺的,因此它们在每个平台上都有明确的结果。

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

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

是的,往返小数可以做到这一点。此外,您真的应该将lookup tables 用于trig (sin, cos) 函数;因为它们可以真的在不同的平台上给出不同的结果 - 如果你正确编码它们,它们可以直接使用 QNumbers。

【讨论】:

不确定你在说什么帧率问题。显然,您希望有一个固定的更新率(例如参见here)——这与显示帧率是否相同无关紧要。只要所有机器上的误差都相同,我们就很好。我完全不明白你的第三个答案。 @BlueRaja:答案“有没有办法强制我的程序以双精度运行?”要么相当于重新实现整个公共语言运行时,这将非常复杂,要么使用从 C# 应用程序对 C++ DLL 的本机调用,正如用户 shelleybutterfly 的回答所暗示的那样。正如我的回答所暗示的那样,将“QNumbers”仅仅视为二进制定点数(直到现在我还没有看到二进制定点数被称为“QNumbers”。) @Pieter O。您不需要重新实现运行时。我在公司工作的服务器将 CLR 运行时作为本机 C++ 应用程序托管(SQL Server 也是如此)。我建议你用谷歌搜索 CorBindToRuntimeEx。 @BlueRaja 这取决于所讨论的游戏。对所有游戏应用固定帧率步骤不是一个可行的选择——因为 AOE 算法引入了人为延迟;这是不可接受的,例如FPS。 @Jonathan:这只是点对点游戏中的一个问题,它只发送输入 - 对于这些,你必须有一个固定的更新率。大多数 FPS 不是这样工作的,但少数确实有固定的更新率。见this question。【参考方案5】:

根据这个有点老的MSDN blog entry,JIT 不会将 SSE/SSE2 用于浮点,它都是 x87。因此,正如您提到的,您必须担心模式和标志,而在 C# 中这是无法控制的。因此,使用普通的浮点运算并不能保证您的程序在每台机器上都得到完全相同的结果。

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

根据您需要的操作,您也许可以使用单精度。想法是这样的:

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

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

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

希望这会有所帮助!

【讨论】:

.NET 5,或6,或42可能不再使用x87计算模式也是一个问题。标准中没有任何要求。【参考方案6】:

正如其他答案所述: 是的,这是 C# 中的一个问题——即使是纯 Windows。

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

按照 OP 的要求 - 关于性能:

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

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

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

为了减少对性能的影响,有几种策略 - 例如 QNumbers(请参阅 Jonathan Dickinson 的回答 - Is floating-point math consistent in C#? Can it be?)和/或缓存(例如三角计算...)等。

【讨论】:

请注意,BigInteger 仅在 .Net 4.0 中可用。 我的猜测是BigInteger 的性能甚至超过了 Decimal 的性能。 这里的答案中有几次提到了使用Decimal(@Jonathan Dickinson - 'dog slow')或BigInteger(上面的@CodeInChaos 评论)的性能损失 - 可以有人请提供一些关于这些性能影响的解释,以及它们是否/为什么真的是提供解决方案的阻碍。 @Yahia - 感谢您的编辑 - 有趣的阅读,但是,您能否就不使用“浮动”的性能影响给出一个大致的猜测,我们说的是 10%慢或慢 10 倍 - 我只是想了解隐含的数量级。 比“只有 10%”更可能在 1:5 的范围内【参考方案7】:

嗯,这是我第一次尝试如何做到这一点

    创建一个 ATL.dll 项目,其中包含一个用于关键浮点运算的简单对象。确保使用禁止使用任何非 xx87 硬件进行浮点运算的标志对其进行编译。 创建调用浮点运算并返回结果的函数;从简单开始,然后如果它适合您,您可以随时增加复杂性以满足您以后的性能需求。 将 control_fp 调用放在实际的数学运算周围,以确保它在所有机器上以相同的方式完成。 引用您的新库并进行测试,以确保它按预期工作。

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

然后,假设它有效,如果你想使用 Mono,我想你应该能够以类似的方式在其他 x86 平台上复制库(当然不是 COM;虽然,也许,用 wine?有点不不过,一旦我们去那里,我所在的地区......)。

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

【讨论】:

"编译成 32 位 .dll 然后使用 ... AnyCpu" 我认为这只有在 32 位系统上运行时才有效。在 64 位系统上,只有以x86 为目标的程序才能加载 32 位 dll。【参考方案8】:

我不是游戏开发者,虽然我确实有很多计算难题的经验......所以,我会尽力而为。

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

使用较慢(如有必要;如果有更快的方法,那就太好了!)但可预测的方法来获得可重复的结果 对其他一切使用双精度(例如渲染)

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

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

【讨论】:

【参考方案9】:

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

【讨论】:

总是将每个操作截断为 30 或 31 位 - 这正是 float 数据类型在 x86 机器上所做的 - 但是这会导致与执行此操作的机器略有不同的结果他们所有的计算都只使用 32 位,这些小的变化会随着时间的推移而传播。因此,问题。 如果“N 位精度”表示任何计算都精确到那么多位,并且机器 A 精确到 32 位,而机器 B 精确到 48 位,那么任何计算的前 32 位两台机器应该是相同的。每次操作后都不会截断到 32 位或更少以保持两台机器完全同步吗?如果不是,有什么例子?【参考方案10】:

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

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

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

四舍五入调整造成的“错误”会随着进一步的说明而增加。

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

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

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

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

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

【讨论】:

想象一个 P2P 射击游戏。你向一个人开枪,你打他,他死了,但它非常接近,你几乎错过了。在另一个人的 PC 上使用略有不同的计算,它计算出你错过了。你现在看到问题了吗?在这种情况下,增加寄存器的大小将无济于事(至少不是完全)。在每台计算机上使用完全相同的计算。 在这种情况下,人们通常不关心结果与实际结果的接近程度(只要它是合理的),但重要的是它完全对所有用户都一样。 你说得对,我没想到这种情况。但是我同意@CodeInChaos 对此的看法。我没有发现两次做出重要决定真的很聪明。这更像是一个软件架构问题。一个程序,例如射手的应用程序,应该进行计算并将结果发送给其他程序。您将永远不会以这种方式出现错误。你有没有命中,但只有一个人做出决定。就像说@driushkin @Aesgar:是的,这就是大多数射手的工作方式;那个“权威”被称为服务器,我们将整体架构称为“客户端/服务器”架构。但是,还有另一种架构:点对点。在 P2P 中,没有服务器;相反,所有客户端必须在发生任何事情之前相互验证所有操作。这会增加延迟,使射击游戏无法接受,但会大大降低网络流量,非常适合可以接受小延迟(~250ms)但不能同步整个游戏状态的游戏.即 C&C 和星际争霸等 RTS 游戏使用 P2P。 在 p2p 游戏中,您没有可信赖的机器可以依赖。如果您允许一个电台决定他的子弹是否击中,您就会打开客户作弊的可能性。此外,链接甚至无法处理有时产生的数据量——游戏通过发送订单而不是结果来工作。我玩 RTS 游戏,很多次我看到这么多垃圾到处乱飞,根本不可能通过普通的家庭上行链路发送。

以上是关于C# 中的浮点数学是不是一致?是真的吗?的主要内容,如果未能解决你的问题,请参考以下文章

hadoop pig - 用逗号加载浮点数

打乱一个浮点数?

踩过了这些坑,你真的懂python基础吗?

为啥 C# 允许在浮点类型中将非零数除以零?

python浮点数是啥意思

为啥 SQL 浮点数与 C# 浮点数不同