不,这不是另一个"为什么是(1/3.0)*3!= 1"问题。
最近我读了很多关于浮点的文章,特别是,相同的计算如何在不同的架构或优化设置上产生不同的结果。
这是存储重播的视频游戏的一个问题,或者是对等网络(与服务器客户端相反),它依赖于所有客户端每次运行程序时都会生成完全相同的结果-一个浮点计算中的微小差异可能会导致不同计算机(甚至是同一台机器!)
这甚至发生在"遵循"IEEE-754的处理器中,主要是因为某些处理器(即x86)使用双扩展精度。也就是说,它们使用80位寄存器进行所有计算,然后截断为64位或32位,从而产生与使用64位或32位进行计算的机器不同的舍入结果。
我已经在网上看到了几个解决这个问题的方法,但都是C++,而不是C语言:
- 禁用双扩展精度模式(以便所有double计算都使用ieee-754 64位),使用_controlfp_s(Windows)、_FPU_SETCW(Linux?)或fpsetprec(BSD)。
- 始终使用相同的优化设置运行相同的编译器,并要求所有用户具有相同的CPU架构(无跨平台播放)。因为我的"编译器"实际上是JIT,它每次运行程序时都可能进行不同的优化,所以我认为这是不可能的。
- 使用定点算法,避免float和double。decimal可以用于此目的,但速度会慢得多,并且没有任何System.Math库功能支持它。
所以,这是C中的一个问题吗?如果我只想支持Windows(而不是Mono)呢?
如果是的话,有没有办法强迫我的程序以正常的双精度运行?
如果没有,是否有任何库可以帮助保持浮点计算的一致性?
- 我看过这个问题,但是每个答案要么重复这个问题,没有解决方案,要么说"忽略它",这不是一个选择。我在GAMDEV上提出了一个类似的问题,但是(因为观众)大多数的答案似乎都是面向C++的。
- 没有答案,但我相信在大多数领域中,您可以以这样一种方式设计您的系统:所有共享状态都是确定性的,因此不会有明显的性能下降。
- 您知道浮点模拟吗?如果关注浮点操作的绝对可移植性,那么处理浮点仿真的库可能会有所帮助。不过,我不知道有什么,在这个过程中可能要牺牲一些表现。
- @彼得,你知道.NET的任何快速浮点模拟吗?
- @对不起,不。
- 这里有一些关于system.decimal的信息-也许它很有趣:csharpindepth.com/articles/general/decimal.aspx
- 您打算在项目中使用什么浮点运算和数学函数?我目前正在开发一个64位浮点库(请参阅我的答案),希望知道您希望在我的库中看到哪些功能。
- decimal本质上不是一种浮点仿真吗?
- @重力是的。但它不适合玩游戏。
- Java有这个问题吗?
- JOHH:Java具有EDCOX1×1 }关键字,它迫使所有的计算都在指定的大小(EDCOX1,2,EDCX1,3)中进行,而不是扩展的大小。然而,Java仍然存在许多IEEE-74支持的问题。很少有编程语言能很好地支持IEE-754。
- 相关:x86汇编语言为您提供了确定性fp(除了rsqrtps之外),但诀窍是获得相同或相似的源代码,以便始终编译相同的源代码。如果你允许不同的编译器,即使在C/C++中也是有问题的。相关:在基于x86的体系结构中,是否有任何浮点密集型代码产生位精确结果?但是,至少对于提前编译的语言,如果避免在不同平台上使用任何允许不同的fp库函数,通常可以获得确定性。
我不知道如何使.NET中的普通浮点具有确定性。允许抖动创建在不同平台(或.NET的不同版本)上行为不同的代码。因此,在确定性.NET代码中使用普通的float是不可能的。
我考虑的解决方法是:
在C_中实现FixedPoint32。虽然这并不太难(我有一个完成了一半的实现),但是值的范围很小,使用起来很麻烦。你必须时刻小心,这样你既不会溢出,也不会失去太多的精度。最后,我发现这并不比直接使用整数容易。
在C_中实现FixedPoint64。我觉得这很难做到。对于某些操作,128位的中间整数是有用的。但是.NET不提供这样的类型。
实现自定义32位浮点。在实现这一点时,缺乏位扫描反转的内在特性会导致一些麻烦。但目前我认为这是最有希望的途径。
使用本机代码进行数学运算。在每次数学运算中都会产生委托调用的开销。
我刚刚开始了32位浮点数学的软件实现。它可以在我的2.66GHz i3上每秒做大约7000万次加法/乘法。https://github.com/codesinchaos/softfloat.很明显,它仍然很不完整,而且很麻烦。
- 有一个"无限"大小的整数,虽然没有原生整数快,也没有原生整数长,所以.NET确实提供了这样的类型(我相信是为f创建的,但可以在c中使用)
- 另一个选项是.NET的GNU MP包装器。它是围绕GNU多精度库的一个包装器,支持"有限"精度整数、有理数(分数)和浮点数。
- 如果你想做这些,你最好先试试decimal,因为这样做要简单得多。只有当它对于手头的任务来说太慢时,其他的方法才是值得考虑的。
- 我了解到一个特殊的情况,其中浮点是确定性的。我得到的解释是:对于乘法/除法,如果其中一个fp数是两个数的幂(2^x),那么在计算过程中有效/尾数不会改变。只有指数会改变(点会移动)。所以四舍五入永远不会发生。结果将是决定性的。
- 例如:2^32这样的数字表示为(指数:32,尾数:1)。如果我们用另一个浮点(exp,man)乘以这个值,结果是(exp+32,man*1)。对于部门,结果是(世博会-32,Man*1)。尾数乘以1并不会改变尾数,所以它有多少位并不重要。
- 为投反对票道歉。我在电话上(如果那是一个词)打错了,现在我无法更改。
C规范(§4.1.6浮点类型)专门允许使用高于结果精度的精度进行浮点计算。所以,不,我认为你不能直接在.NET中使这些计算具有确定性。其他人建议了各种各样的解决办法,所以你可以试试。
- 我刚刚意识到,如果分发已编译的程序集,C规范实际上并不重要。只有想要源代码兼容性才重要。真正重要的是clr规范。但我很肯定这是因为担保和C担保一样薄弱。
- 在一次操作之后,是否每次都向double强制转换,以除去不需要的位,从而产生一致的结果?
- @伊利丹4我不认为这能保证一致的结果。
如果您需要这种操作的绝对可移植性,下面的页面可能会很有用。它讨论了测试IEEE754标准实现的软件,包括模拟浮点运算的软件。然而,大多数信息可能是特定于C或C++的。
http://www.math.utah.edu/~beebe/software/ieee/犹他州/
关于定点的注记
二进制定点数也可以很好地代替浮点数,这从四种基本算术运算中可以明显看出:
- 加减法很简单。它们的工作方式与整数相同。只需加或减!
- 要使两个定点数字相乘,请将两个数字相乘,然后向右移动定义的小数位数。
- 要将两个定点数相除,请将被除数左移指定的小数位数,然后除以除数。
- 本文第四章对二元不动点数的实现作了进一步的指导。
二进制定点数可以在任何整数数据类型上实现,如int、long和bigginteger,以及不符合CLS的类型uint和ulong。
正如在另一个答案中建议的那样,您可以使用查找表,其中表中的每个元素都是一个二进制定点数字,以帮助实现复杂的函数,如正弦、余弦、平方根等等。如果查找表的粒度小于固定点号,建议将查找表粒度的一半添加到输入中,对输入进行四舍五入:
1 2 3 4 5 6 7 8
| // 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的类型,如ulong、uint等,以提高速度-因为它们很少使用,所以JIT并没有像使用long和int这样积极地优化它们,因此使用它们实际上比使用普通类型慢。此外,C具有运算符重载,该项目将从中受益匪浅。最后,是否有关联的单元测试?除了这些小事情,惊人的工作彼得,这是荒谬的令人印象深刻!
- 谢谢你的评论。我确实在代码上执行单元测试。不过,它们相当广泛,目前还不能发布。我甚至编写单元测试助手例程来简化编写多个测试。我现在不使用重载操作符,因为我已经完成了把代码翻译成Java的计划。
- 有趣的是,当我在你的博客上发表文章时,我没有注意到博客是你的。我刚决定尝试一下Google+,在它的C Spark中,它提出了这个博客条目。所以我想,"我们俩同时开始写这样的东西,真是太巧了。"但我们当然也有同样的动机:)
- 为什么要把这个移植到Java?Java已经通过EDCOX1(10)保证了确定性浮点数学。
这是C的问题吗?
对。不同的体系结构是您最不担心的,不同的帧速率等可能会由于浮点表示的不准确而导致偏差-即使它们是相同的不准确(例如相同的体系结构,除了一台机器上较慢的GPU)。
我可以用System.Decimal吗?
你没有理由不能,不过这是狗慢。
有没有办法强迫我的程序以双精度运行?
对。自己调用CLR运行库;在调用CorBindToRuntimeEx之前,将所有的NICE调用/标志(改变浮点运算的行为)编译成C++应用程序。
是否有任何库可以帮助保持浮点计算的一致性?
我不知道。
还有别的方法可以解决这个问题吗?
我以前解决过这个问题,我的想法是使用Qnumbers。它们是固定点的real形式;但不是以10为基数(十进制)的固定点,而不是以2为基数(二进制)的固定点;因此,它们上面的数学原语(add、sub、mul、div)比简单的10为基数的固定点快得多;特别是如果两个值的n相同(在您的情况下是这样)。此外,因为它们是整体的,所以在每个平台上都有明确的结果。
请记住,帧速率仍然会影响这些,但它并没有那么糟糕,而且很容易使用同步点纠正。
我可以用更多的数学函数来表示Q数吗?
是的,往返一个十进制数。此外,您真的应该为trig(sin,cos)函数使用查找表;因为它们可以在不同的平台上给出不同的结果——如果您正确地编码它们,它们可以直接使用qnumber。
- 不知道你在说什么,因为帧速率有问题。很明显,您希望有一个固定的更新率(请参见此处的示例)-无论这是否与显示帧速率相同,都是不相关的。只要所有机器的误差都一样,我们就很好。我完全不明白你的第三个回答。
- @蓝调:答案是"有没有办法强迫我的程序以双精度运行?"要么将重新实现整个公共语言运行时,这将是极其复杂的,要么使用来自C++应用程序的C++ DLL的本地调用,如用户Sely蝴蝶的答案所暗示的那样。把"Qnumbers"想象成二进制定点数,正如我的答案中暗示的那样(直到现在我才看到二进制定点数被称为"Qnumbers")。
- @你不需要重新实现运行时。我在我公司工作的服务器将CLR运行时托管为本地C++应用程序(SQLServer也是如此)。我建议你搜索corbindtoruntimeex。
- @蓝调,这取决于游戏的问题。将固定帧速率步骤应用于所有游戏并不是一个可行的选择,因为AOE算法引入了人工延迟,这在例如fps中是不可接受的。
- @乔纳森:这只是点对点游戏中的一个问题,它只发送输入-对于这些游戏,你必须有一个固定的更新率。大多数fps的工作方式不是这样的,但只有少数几个有固定的更新率。看看这个问题。
- @布鲁拉贾,谢谢你。相当有信息性——我仍然认为固定点在固定时间步长的场景中很重要(但不那么重要),因为毕竟你是在操作系统调度器的控制之下(但同样,这也没那么糟糕)。
根据这个稍微旧一点的msdn博客条目,jit不会使用sse/sse2作为浮点,它都是x87。因此,正如您所提到的,您必须担心模式和标志,而在C中,这是不可能控制的。因此,使用正常的浮点运算并不能保证在您的程序中的每台机器上都有完全相同的结果。
要获得双精度的精确再现性,您需要进行软件浮点(或定点)仿真。我不知道C图书馆会这么做。
根据您需要的操作,您可能能够以单精度逃脱。这里的想法是:
- 以单精度存储您关心的所有值
- 要执行操作:
- 将输入扩展到双精度
- 双精度操作
- 将结果转换回单精度
x87的一个大问题是,根据精度标志以及寄存器是否溢出到内存中,可以用53位或64位精度进行计算。但对于许多操作来说,高精度执行操作和舍入到低精度将保证正确答案,这意味着所有系统的答案都是相同的。你是否得到额外的精确性并不重要,因为你有足够的精确性来保证在这两种情况下的正确答案。
应在此方案中工作的操作:加法、减法、乘法、除法、sqrt。像sin、exp等的东西是行不通的(结果通常是匹配的,但没有保证)。什么时候双料倒圆是无害的?"ACM参考(付费注册Req)
希望这有帮助!
- 还有一个问题是.NET 5、6或42可能不再使用X87计算模式。标准中没有要求的内容。
如其他答案所述:是的,这是C中的一个问题,即使保持纯窗口。
对于解决方案:如果您使用内置的BigInteger类,并通过使用公分母对这些数字的任何计算/存储将所有计算扩展到定义的精度,则可以完全减少(并通过一些努力/性能影响)该问题。
根据运营商的要求-关于性能:
System.Decimal表示一个数字,符号为1位,整数为96位,"小数位数"(表示小数点的位置)。对于所有的计算,它必须在这个数据结构上运行,不能使用任何内置在CPU中的浮点指令。
BigInteger的"解决方案"做了一些类似的事情——只是你可以定义你需要/想要多少数字…也许您只需要80位或240位的精度。
慢度总是来自于在不使用CPU/FPU内置指令的情况下,通过仅整数指令模拟这些数字上的所有操作,而这反过来又会导致每个数学操作产生更多的指令。
为了减少性能冲击,有几种策略-如Qnumbers(见Jonathan Dickinson的答案)-浮点数学在C中是否一致?可以吗?)和/或缓存(例如trig calculations…)等。
- 请注意,BigInteger仅在.NET 4.0中可用。
- 我的猜测是,BigInteger的性能命中甚至超过了十进制的性能命中。
- 在这里的答案中,有几次提到了使用decimal(@jonathan dickinson-"dog slow")或BigInteger(@codeinchaos comment above)的性能冲击,有人能就这些性能冲击提供一些解释,以及他们是否真的/为什么会停止提供解决方案。
- 查看我上面的编辑
- @Yahia-谢谢你的编辑-有趣的阅读,但是,你能不能也给一个棒球场的猜测关于不使用"浮动"的性能命中我们是说10%慢或10倍慢-我只想得到一个数量级隐含的感觉。
- 在1:5的范围内,它更像是一个"只有10%的人"。
我不是一个游戏开发者,虽然我在计算难题方面有很多经验…所以,我会尽力的。
我将采用的策略基本上是:
- 使用较慢的(如果需要;如果有更快的方法,很好!)可预测的方法,以获得可重复的结果
- 其他一切都使用double(例如渲染)
这其中的长短是:你需要找到一个平衡点。如果你花费30毫秒的渲染时间(~33fps),而只有1毫秒的时间进行碰撞检测(或插入一些其他高度敏感的操作),即使你花了三倍的时间来完成关键的算术运算,它对你的帧速率的影响也会从33.3fps下降到30.3fps。
我建议你对每件事情都进行分析,说明每一个明显昂贵的计算花费了多少时间,然后用1种或更多的方法重复测量来解决这个问题,看看会有什么影响。
好吧,这是我第一次尝试如何做到这一点:
创建一个atl.dll项目,该项目中有一个简单对象,可用于关键的浮点操作。确保使用禁止使用任何非xx87硬件执行浮点运算的标志编译它。
创建调用浮点运算并返回结果的函数;从简单的开始,然后如果它对您有效,您可以随时增加复杂性,以满足以后的性能需求(如果需要)。
把控制调用放在实际的数学周围,以确保在所有机器上都以相同的方式进行。
参考您的新库并进行测试,以确保它按预期工作。
(我相信您可以编译为32位.dll,然后将其与x86或anycpu一起使用[或者可能仅针对64位系统上的x86;请参阅下面的注释]。)
那么,假设它有效,您是否应该使用mono?我想您应该能够以类似的方式在其他x86平台上复制库(当然不是com;尽管,也许是葡萄酒?不过,一旦我们去了那里,就有点不在我的区域了……)。
假设您可以使其工作,您应该能够建立自定义函数,可以同时进行多个操作来修复任何性能问题,并且您将具有浮点数学,允许您在跨平台上具有一致的结果,其中以C++编写的代码量最少,并且将其余代码保留在C++中。
- "编译为32位.dll,然后使用…anycpu"我认为这只能在32位系统上运行时工作。在64位系统上,只有以x86为目标的程序才能加载32位dll。
- 谢谢;更新答案
检查其他答案中的链接可以清楚地表明,您将永远无法保证浮点是否"正确"实现,或者对于给定的计算,您是否总是能够获得一定的精度,但也许您可以尽最大努力(1)将所有计算截断到一个公共的最小值(例如,如果不同的实现将为您提供32到80位的精度,总是将每个操作截断为30或31位,(2)在启动时有一个包含一些测试用例的表(加减、乘、除、sqrt、cosine等的边界用例),如果实现计算出与表匹配的值,则不需要进行任何调整。
- 总是将每个操作截断为30或31位—这正是float数据类型在x86计算机上所做的—但是这将导致与仅使用32位进行所有计算的计算机的结果略有不同,并且这些小的更改将随时间传播。因此,这个问题。
- 如果"n位精度"意味着任何计算都精确到这许多位,并且机器A精确到32位,而机器B精确到48位,那么两台机器计算的前32位应该相同。在每次操作后,是否都将截短到32位或更少,以保持两台机器完全同步?如果没有,那有什么例子呢?
你的问题很难,技术性很强。不过我可能有个主意。
您肯定知道CPU在任何浮动操作之后都会做一些调整。CPU提供了几种不同的指令,使得取整操作不同。
因此,对于表达式,编译器将选择一组指令,引导您得到结果。但是任何其他指令工作流,即使它们打算计算相同的表达式,也可以提供另一个结果。
四舍五入调整所犯的"错误"将在每一个进一步的说明中增加。
作为一个例子,我们可以说在装配级别:a*b*c不等于a*c*b。
我不完全确定,你需要找一个比我更了解CPU架构的人:p
然而,回答你的问题:在C或C++中,你可以解决你的问题,因为你对编译器生成的机器代码有一定的控制,但是.NET中你没有任何控制。因此,只要您的机器代码不同,您就永远无法确定确切的结果。
我很好奇这在哪方面会是一个问题,因为变化看起来非常小,但是如果你真的需要精确的操作,我唯一能想到的解决方案是增加浮动寄存器的大小。如果可以,可以使用双精度甚至长双精度(不确定使用CLI是否可行)。
我希望我已经足够清楚了,我的英语不太好(…一点也不好)
- 想象一个P2P射手。你朝一个男人开枪,你打他,他就死了,但离得很近,你差点就错过了。在另一个人的电脑上使用稍微不同的计算方法,它计算出你错过的。你现在看到问题了吗?在这种情况下,增加寄存器的大小是没有帮助的(至少不是完全的)。在每台计算机上使用完全相同的计算。
- 在这种情况下,人们通常不关心结果与实际结果的接近程度(只要它是合理的),但重要的是,它对所有用户都是完全相同的。
- 你说得对,我没有考虑过这种情况。不过我同意@codeinchaos的说法。我觉得两次做出重要决定并不明智。这更像是一个软件体系结构问题。一个程序,例如射手的应用程序,应该进行计算并将结果发送给其他程序。这样你就不会有错误。你有没有命中,但只有一个能决定。比如说@driushkin
- @埃斯加:是的,这就是大多数射手的工作方式;这种"权威"被称为服务器,我们称整个体系结构为"客户机/服务器"体系结构。然而,还有另一种架构:点对点。在p2p中,没有服务器;相反,所有客户机必须在发生任何事情之前相互验证所有操作。这会增加延迟,使射手无法接受,但会极大地减少网络流量,使其非常适合可以接受小延迟(约250毫秒)但不能同步整个游戏状态的游戏。也就是说,像C&C和星际争霸这样的即时战略游戏使用P2P。
- 如果只有服务器可以射击,那将是一个奇怪的游戏:d.p2p是一个通信协议,但它不会停止信息:"这一个拿着我的子弹。"如果只有一个程序测试它,你甚至可以有时间改进更复杂的物理。更重要的是,由于硬件限制,它将忽略您的错误!
- 如果输入完全相同,这种网络代码依赖于所有参与计算机上以完全相同的方式运行的代码。然后它只发送输入。这方面的好处是,即使你有很多物体,交通量也很低。因此,它被用于几乎所有的即时战略游戏,我见过。关于这个的一篇老文章:gamasutra.com/view/feature/3094/&hellip;
- 在P2P游戏中,你没有可信赖的机器。如果你允许一个电台决定他的子弹是否击中,你就有可能让客户作弊。此外,链接甚至无法处理有时会产生结果的数据量——游戏通过发送命令而不是结果来工作。我玩即时战略游戏,很多次我都看到垃圾飞来飞去,无法通过正常的家庭上传发送。