提问人:BlueRaja - Danny Pflughoeft 提问时间:7/14/2011 最后编辑:CommunityBlueRaja - Danny Pflughoeft 更新时间:12/5/2021 访问量:18032
浮点数学在 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#:
- 使用
_controlfp_s
(Windows)、(Linux?) 或fpsetprec
(BSD) 禁用双扩展精度模式(以便所有计算都使用 IEEE-754 64 位)。double
_FPU_SETCW
- 始终使用相同的优化设置运行相同的编译器,并要求所有用户具有相同的 CPU 架构(无跨平台游戏)。因为我的“编译器”实际上是 JIT,每次运行程序时它可能会以不同的方式进行优化,所以我认为这是不可能的。
- 使用定点算术,并完全避免和。 将用于此目的,但速度会慢得多,并且没有任何库函数支持它。
float
double
decimal
System.Math
那么,这在 C# 中甚至是一个问题吗?如果我只打算支持 Windows(而不是 Mono)怎么办?
如果是,有没有办法强制我的程序以正常的双精度运行?
如果没有,是否有任何库可以帮助保持浮点计算的一致性?
答:
如果您需要此类操作的绝对可移植性,以下页面可能会很有用。它讨论了用于测试 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];
评论
const
static
myDouble.LeadingZeros()
IntDouble.LeadingZeros(myDouble)
MultiplyAnyLength
unchecked
ulong
uint
long
int
strictfp
你的问题在相当困难和技术性的东西O_o。但是,我可能有一个想法。
您肯定知道 CPU 在任何浮动操作后都会进行一些调整。 CPU 提供了几种不同的指令,这些指令可以进行不同的舍入操作。
因此,对于表达式,编译器将选择一组指令来引导您获得结果。但是,任何其他指令工作流,即使它们打算计算相同的表达式,也可以提供另一个结果。
舍入调整所犯的“错误”将在每次进一步的指令中增加。
举个例子,我们可以说,在汇编级别上:a * b * c 不等同于 a * c * b。
我不完全确定,你需要找一个比我更了解 CPU 架构的人:p
但是,要回答您的问题:在 C 或 C++ 中,您可以解决问题,因为您可以对编译器生成的机器代码进行一些控制,但是在 .NET 中,您没有任何控制权。因此,只要您的机器代码可能不同,您就永远无法确定确切的结果。
我很好奇这在哪种方式上可能是一个问题,因为变化似乎非常小,但如果您需要真正准确的操作,我能想到的唯一解决方案就是增加浮动寄存器的大小。如果可以的话,使用双精度甚至长双精度(不确定使用 CLI 是否可行)。
我希望我已经足够清楚了,我的英语并不完美(...全部 : s)
评论
我不知道没有办法在.net中使普通浮点确定性。允许 JITter 创建在不同平台(或不同版本的 .net)上行为不同的代码。因此,在确定性的 .net 代码中使用普通 s 是不可能的。float
我考虑的解决方法:
- 在 C# 中实现 FixedPoint32。虽然这并不难(我有一个半成品的实现),但值的范围非常小,使用起来很烦人。你必须时刻小心,这样你既不会溢出,也不会失去太多的精度。最后,我发现这并不比直接使用整数容易。
- 在 C# 中实现 FixedPoint64。我发现这很难做到。对于某些操作,128 位的中间整数会很有用。但 .net 不提供这样的类型。
- 实现自定义 32 位浮点。缺少 BitScanReverse 内部函数会导致在实现此操作时出现一些烦恼。但目前我认为这是最有希望的道路。
- 使用本机代码进行数学运算。在每个数学运算上产生委托调用的开销。
我刚刚开始了 32 位浮点数学的软件实现。它在我的 2.66GHz i3 上每秒可以进行大约 7000 万次加法/乘法。https://github.com/CodesInChaos/SoftFloat .显然,它仍然非常不完整且有问题。
评论
decimal
C# 规范(§4.1.6 浮点类型)明确允许使用高于结果的精度进行浮点计算。所以,不,我认为你不能直接在.Net中使这些计算具有确定性。其他人提出了各种解决方法,因此您可以尝试一下。
评论
double
根据这篇稍旧的 MSDN 博客文章,JIT 不会使用 SSE/SSE2 作为浮点,它都是 x87。因此,正如您提到的,您必须担心模式和标志,而在 C# 中,这是无法控制的。因此,使用普通的浮点运算并不能保证程序在每台机器上获得完全相同的结果。
为了获得双精度的精确再现性,您必须进行软件浮点(或定点)仿真。我不知道 C# 库可以做到这一点。
根据您需要的操作,您也许能够摆脱单一精度。这个想法是这样的:
- 以单个精度存储您关心的所有值
- 要执行操作:
- 将输入扩展到双精度
- 双精度DO操作
- 将结果转换回单精度
x87 的最大问题是计算可能以 53 位或 64 位精度完成,具体取决于精度标志以及寄存器是否溢出到内存中。但对于许多操作,以高精度执行操作并四舍五入到较低精度将保证正确答案,这意味着答案将保证在所有系统上都是相同的。你是否获得额外的精度并不重要,因为你有足够的精度来保证在任何一种情况下都能得到正确的答案。
在此方案中应该起作用的运算:加法、减法、乘法、除法、sqrt。诸如sin,exp等之类的东西是行不通的(结果通常会匹配,但不能保证)。“什么时候双舍五入是无害的?”ACM 参考(付费注册要求)
希望这有帮助!
评论
正如其他答案已经指出的那样: 是的,这是 C# 中的一个问题 - 即使保持纯 Windows。
至于解决方案:
如果使用内置类,并通过对此类数字的任何计算/存储使用公分母将所有计算缩放到定义的精度,则可以完全避免该问题(并降低一些工作量/性能)。BigInteger
根据 OP 的要求 - 关于性能:
System.Decimal
表示数字,其中 1 位表示符号,96 位整数和“小数位数”(表示小数点的位置)。对于您进行的所有计算,它必须对此数据结构进行操作,并且不能使用 CPU 中内置的任何浮点指令。
“解决方案”做了类似的事情 - 只是您可以定义您需要/想要多少位数......也许您只想要 80 位或 240 位的精度。BigInteger
缓慢总是来自必须通过纯整数指令模拟这些数字的所有操作,而不使用 CPU/FPU 内置指令,这反过来又导致每个数学运算的指令要多得多。
为了减少对性能的影响,有几种策略 - 比如 QNumbers(参见 Jonathan Dickinson 的回答 - 浮点数学在 C# 中是一致的吗?可以吗?和/或缓存(例如trig计算...)等。
评论
BigInteger
BigInteger
Decimal
BigInteger
好吧,这将是我第一次尝试如何做到这一点:
- 创建一个 ATL.dll 项目,其中包含用于关键浮点运算的简单对象。确保使用禁止使用任何非 xx87 硬件执行浮点运算的标志来编译它。
- 创建调用浮点运算并返回结果的函数;从简单开始,然后如果它适合您,您可以随时增加复杂性以满足以后的需求(如有必要)。
- 将control_fp调用放在实际的数学运算上,以确保在所有机器上都以相同的方式完成。
- 引用您的新库并进行测试,以确保其按预期工作。
(我相信你可以编译成一个32位的.dll,然后用x86或AnyCpu[或者可能只针对64位系统上的x86;见下面的注释]。
然后,假设它有效,如果你想使用 Mono,我想你应该能够以类似的方式在其他 x86 平台上复制该库(当然不是 COM;虽然,也许,用 wine?一旦我们去那里,就有点超出我的区域......
假设您可以使其工作,您应该能够设置可以一次执行多个操作的自定义函数来修复任何性能问题,并且您将拥有浮点数学,允许您使用最少的代码量在 C++ 中跨平台获得一致的结果,并将其余代码保留在 C# 中。
评论
x86
这是 C# 的问题吗?
是的。不同的架构是您最不担心的,不同的帧速率等可能会导致由于浮点表示不准确而导致偏差 - 即使它们是相同的不准确(例如,相同的架构,除了一台机器上的 GPU 速度较慢)。
我可以使用 System.Decimal 吗?
你没有理由不能,但它很慢。
有没有办法强制我的程序以双精度运行?
是的。自行承载 CLR 运行时;并在调用 CorBindToRuntimeEx 之前将所有 nessecary 调用/标志(更改浮点运算的行为)编译到 C++ 应用程序中。
是否有任何库可以帮助保持浮点计算的一致性?
我不知道。
有没有其他方法可以解决这个问题?
我以前已经解决了这个问题,我的想法是使用 QNumbers。它们是定点实数的一种形式;但不是以 10 为基数(十进制)的固定点 - 而是以 2 为基数(二进制);正因为如此,它们上的数学基元(add、sub、mul、div)比朴素的以 10 为底的不动点要快得多;特别是如果两个值相同(在您的情况下是相同的)。此外,由于它们是不可或缺的,因此它们在每个平台上都有明确定义的结果。n
请记住,帧速率仍然会影响这些,但它并没有那么糟糕,并且很容易使用同步点进行纠正。
我可以在 QNumbers 中使用更多数学函数吗?
是的,往返小数点即可执行此操作。此外,您确实应该对 trig (sin, cos) 函数使用查找表;因为这些确实可以在不同的平台上给出不同的结果 - 如果你正确地编码它们,它们可以直接使用 QNumbers。
评论
检查其他答案中的链接可以清楚地看出,你永远无法保证浮点是否“正确”实现,或者你是否总是能获得给定计算的一定精度,但也许你可以尽最大努力(1)将所有计算截断到一个共同的最小值(例如,如果不同的实现会给你32到80位的精度, 总是将每个操作截断为 30 或 31 位),(2) 在启动时有一个包含一些测试用例的表格(加、减、乘、除、sqrt、余弦等临界情况),如果实现计算的值与表格匹配,则无需费心进行任何调整。
评论
float
我不是游戏开发者,尽管我确实在计算困难的问题方面有很多经验......所以,我会尽力而为。
我采用的策略基本上是这样的:
- 使用较慢(如有必要;如果有更快的方法,那就太好了!),但可预测的方法来获得可重复的结果
- 对其他所有事情(例如,渲染)使用 double
简而言之:你需要找到一个平衡点。如果你花了 30 毫秒渲染 (~33fps),而只有 1 毫秒进行碰撞检测(或插入一些其他高度敏感的操作)——即使你把做关键算术所需的时间增加了三倍,它对帧率的影响是你从 33.3fps 下降到 30.3fps。
我建议你分析所有内容,说明每次计算所花费的时间,然后用一种或多种方法来解决这个问题,看看有什么影响。
评论
strictfp
float
double