ChatGPT解决这个技术问题 Extra ChatGPT

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

不,这不是另一个“为什么是 (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)怎么办?

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

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

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

C
CodesInChaos

我知道没有办法在 .net 中确定正常的浮点数。 JITter 允许创建在不同平台(或不同版本的 .net)上行为不同的代码。因此,在确定性 .net 代码中使用普通 float 是不可能的。

我考虑的解决方法:

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

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


有一个“无限”大小的整数可用 BigInteger 虽然不如本机 int 快或 long 它在那里所以.NET确实提供了这种类型(我相信为 F# 创建但可以在 C# 中使用)
如果您要执行其中任何一项操作,不妨先尝试 decimal,因为这样做要简单得多。只有当手头的任务太慢时,其他方法才值得考虑。
我已经了解了浮点是确定性的一种特殊情况。我得到的解释是:对于乘法/除法,如果 FP 数之一是两个数的幂 (2^x),则在计算过程中有效/尾数不会改变。只有指数会改变(点会移动)。所以舍入永远不会发生。结果将是确定性的。
示例:像 2^32 这样的数字表示为(指数:32,尾数:1)。如果我们将它与另一个浮点数 (exp, man) 相乘,结果是 (exp + 32, man * 1)。对于除法,结果是 (expo - 32, man * 1)。将尾数乘以 1 不会改变尾数,因此它有多少位并不重要。
为反对票道歉。我在手机上误点击(如果那是一个词),现在我无法更改它。
s
svick

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


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

在您需要此类操作的绝对可移植性的情况下,以下页面可能很有用。它讨论了用于测试 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];

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

这是 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
N
Nathan Whitehead

根据这个稍微旧的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 计算模式也是一个问题。标准中没有任何要求。
N
Nikita

正如其他答案已经说明的那样:是的,这是 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 的范围内
s
shelleybutterfly

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

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

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

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

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


“编译为 32 位 .dll,然后使用 ... AnyCpu”我认为这仅在 32 位系统上运行时才有效。在 64 位系统上,只有以 x86 为目标的程序才能加载 32 位 dll。
B
Brian Vandenberg

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

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

使用较慢(如有必要;如果有更快的方法,那太好了!),但可预测的方法来获得可重复的结果

对其他一切使用双精度(例如,渲染)

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

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


m
mike

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


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

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

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

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

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

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

我不完全确定这一点,您需要询问比我更了解 CPU 架构的人:p

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

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

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


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