不,这不是另一个“为什么是 (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) 禁用双精度扩展模式(以便所有双精度计算使用 IEEE-754 64 位)。
始终以相同的优化设置运行相同的编译器,并要求所有用户具有相同的 CPU 架构(不能跨平台播放)。因为我的“编译器”实际上是 JIT,每次运行程序时可能会优化不同,我认为这是不可能的。
使用定点算术,完全避免浮点数和双精度数。十进制可用于此目的,但速度会慢得多,并且没有任何 System.Math 库函数支持它。
那么,这甚至是 C# 中的问题吗?如果我只打算支持 Windows(不是 Mono)怎么办?
如果是,有什么办法可以强制我的程序以正常的双精度运行?
如果没有,是否有任何库可以帮助保持浮点计算的一致性?
strictfp
关键字,它强制所有计算以规定的大小(float
或 double
)而不是扩展大小完成。但是,Java 在 IEE-754 支持方面仍然存在许多问题。非常(非常,非常)少数编程语言能够很好地支持 IEE-754。
我知道没有办法在 .net 中确定正常的浮点数。 JITter 允许创建在不同平台(或不同版本的 .net)上行为不同的代码。因此,在确定性 .net 代码中使用普通 float
是不可能的。
我考虑的解决方法:
在 C# 中实现 FixedPoint32。虽然这并不太难(我已经完成了一半的实现),但非常小的值范围使其使用起来很烦人。您必须始终小心,以免溢出,也不会丢失太多精度。最后我发现这并不比直接使用整数更容易。在 C# 中实现 FixedPoint64。我发现这很难做到。对于某些操作,128 位的中间整数会很有用。但是.net 不提供这种类型。实现一个自定义的 32 位浮点。在实现这一点时,缺少 BitScanReverse 内在函数会导致一些烦恼。但目前我认为这是最有希望的路径。使用本机代码进行数学运算。对每个数学运算产生委托调用的开销。
我刚刚开始了 32 位浮点数学的软件实现。在我的 2.66GHz i3 上,它每秒可以进行大约 7000 万次加法/乘法运算。 https://github.com/CodesInChaos/SoftFloat 。显然它仍然非常不完整和错误。
C# 规范(第 4.1.6 节浮点类型)特别允许使用高于结果的精度进行浮点计算。所以,不,我认为您不能直接在.Net 中使这些计算具有确定性。其他人建议了各种解决方法,因此您可以尝试一下。
double
,从而产生一致的结果?
在您需要此类操作的绝对可移植性的情况下,以下页面可能很有用。它讨论了用于测试 IEEE 754 标准实现的软件,包括用于模拟浮点运算的软件。然而,大多数信息可能特定于 C 或 C++。
http://www.math.utah.edu/~beebe/software/ieee/
关于固定点的说明
二进制定点数也可以很好地代替浮点数,这从四个基本算术运算中可以看出:
加法和减法是微不足道的。它们的工作方式与整数相同。只需加或减!
要将两个定点数相乘,请将这两个数相乘,然后右移定义的小数位数。
要将两个定点数相除,请将被除数左移定义的小数位数,然后除以除数。
Hattangady (2007) 的第四章对实现二进制定点数有额外的指导(SK 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];
const
而不是 static
作为常量,所以编译器可以优化它们;喜欢成员函数而不是静态函数(所以我们可以调用,例如 myDouble.LeadingZeros()
而不是 IntDouble.LeadingZeros(myDouble)
);尽量避免使用单字母变量名(例如,MultiplyAnyLength
有 9,很难理解)
unchecked
和不符合 CLS 的类型(如 ulong
、uint
等)以提高速度 - 因为它们很少使用,JIT 不会积极优化它们,因此实际上可以使用它们比使用 long
和 int
等普通类型慢。此外,C# 有 operator overloading,这个项目将从中受益匪浅。最后,是否有任何相关的单元测试?除了那些小事之外,令人惊叹的工作彼得,这真是令人印象深刻!
strictfp
保证了确定性浮点数学。
这是 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。
根据这个稍微旧的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.)
希望这可以帮助!
正如其他答案已经说明的那样:是的,这是 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 评论)的性能损失 - 有人可以提供一些关于这些的解释性能命中以及它们是否/为什么真的是提供解决方案的障碍。
好吧,这将是我第一次尝试如何做到这一点:
创建一个 ATL.dll 项目,其中包含一个用于关键浮点运算的简单对象。确保使用禁止使用任何非 xx87 硬件进行浮点运算的标志对其进行编译。创建调用浮点运算并返回结果的函数;从简单开始,然后如果它对您有用,您可以随时增加复杂性以满足您以后的性能需求。将 control_fp 调用放在实际数学周围,以确保它在所有机器上以相同的方式完成。参考您的新库并进行测试以确保它按预期工作。
(我相信您可以编译成 32 位 .dll,然后将其与 x86 或 AnyCpu 一起使用[或者可能仅针对 64 位系统上的 x86;请参阅下面的评论]。)
然后,假设它有效,如果你想使用 Mono,我想你应该能够以类似的方式在其他 x86 平台上复制库(当然不是 COM;虽然,也许,用酒?有点超出我的领域一次我们去那里虽然......)。
假设您可以使其工作,您应该能够设置可以一次执行多个操作以解决任何性能问题的自定义函数,并且您将拥有浮点数学,允许您以最少的数量在平台上获得一致的结果用 C++ 编写的代码,而将其余代码留在 C# 中。
x86
为目标的程序才能加载 32 位 dll。
我不是游戏开发者,虽然我确实有很多计算难题的经验……所以,我会尽力而为。
我将采用的策略基本上是这样的:
使用较慢(如有必要;如果有更快的方法,那太好了!),但可预测的方法来获得可重复的结果
对其他一切使用双精度(例如,渲染)
这件事的短处是:你需要找到一个平衡点。如果您花费 30 毫秒渲染 (~33fps) 并且仅 1 毫秒进行碰撞检测(或插入一些其他高度敏感的操作)——即使您将执行关键算术所需的时间增加三倍,它对您的帧速率的影响是你从 33.3fps 下降到 30.3fps。
我建议您对所有内容进行概要分析,说明每个明显昂贵的计算花费了多少时间,然后使用一种或多种解决此问题的方法重复测量,看看会产生什么影响。
检查其他答案中的链接可以清楚地表明,您永远无法保证浮点是否“正确”实现,或者您是否总是会为给定的计算获得一定的精度,但也许您可以尽最大努力(1) 将所有计算截断到一个共同的最小值(例如,如果不同的实现将为您提供 32 到 80 位的精度,则总是将每个操作截断为 30 或 31 位),(2)在启动时有一个包含几个测试用例的表(加、减、乘、除、sqrt、余弦等的边界情况)并且如果实现计算与表匹配的值,则不必费心进行任何调整。
float
数据类型在 x86 机器上所做的 - 但是这将导致与仅使用 32 位进行所有计算的机器的结果略有不同,并且这些小的变化会随着时间的推移而传播。因此,问题。
你的问题是相当困难和技术性的东西 O_o。不过我可能有一个想法。
您肯定知道 CPU 在任何浮动操作后都会进行一些调整。 CPU提供了几种不同的指令来进行不同的舍入操作。
所以对于一个表达式,你的编译器会选择一组指令来引导你得到一个结果。但是任何其他指令工作流程,即使他们打算计算相同的表达式,也可以提供另一个结果。
由四舍五入调整所造成的“错误”会随着每一次进一步的指示而增加。
例如,我们可以说在汇编级别:a * b * c 不等于 a * c * b。
我不完全确定这一点,您需要询问比我更了解 CPU 架构的人:p
但是要回答您的问题:在 C 或 C++ 中,您可以解决您的问题,因为您可以对编译器生成的机器代码进行一些控制,但是在 .NET 中您没有任何控制。因此,只要您的机器代码可能不同,您就永远无法确定确切的结果。
我很好奇这会以何种方式成为问题,因为变化似乎非常小,但如果您需要真正准确的操作,我能想到的唯一解决方案是增加浮动寄存器的大小。如果可以的话,使用双精度甚至长双精度(不确定是否可以使用 CLI)。
我希望我已经足够清楚了,我的英语并不完美(......根本:s)
decimal
,因为这样做要简单得多。只有当手头的任务太慢时,其他方法才值得考虑。