介绍
最近我一直在研究通过确定性帧同步方案模拟在线游戏的物理原理。
通过确定性帧同步方案模拟网络游戏中物理部分的基本思想是不再直接在网络上发送物体的位置、方向、速度等信息来同步物理对象的状态,而是通过发送玩家的模拟来隐式地使用物理对象的输入信息进行同步
这是一个非常有吸引力的同步策略,因为网络流量的大小取决于玩家输入的大小,而不是整个游戏世界状态的大小。事实上,出于这个原因,这种策略已经在实时战略游戏中使用。地图上有成千上万个单位,它们的状态太多,无法通过网络传递。
也许你有一个非常复杂的物理模拟,其中包含大量刚体状态,或者有布料和软体模拟,需要在两台机器上保持非常精确的同步,因为它们的状态会影响游戏,但你无法承受通过网络发送所有状态信息。显然,在这种情况下唯一可能的解决方案是尝试确定性的帧同步方案。
然而,我们遇到了一个问题。物理模拟使用浮点数进行计算,出于这样或那样的原因,在两台不同的机器上得到完全相同的浮点计算结果被认为是非常困难的。甚至有人报道说,同一台机器在不同的时间进行同样的计算,或者使用同一个程序的调试版本和发布版本,会得到不同的结果。还有人说,AMD 的 CPU 和 Intel 的 CPU 会给出不同的计算结果。结果,SSE 和 x87 的计算结果也不同。这是怎么回事?浮点数的计算是确定性的吗?
不幸的是,答案不是简单的“是”或“否”,而是一个模糊的“也许?”
以下是我目前发现的内容:
如果您的物理模拟本身是确定性的,那么通过这些额外的工作,您应该能够在同一台机器上根据记录的玩家输入重播整个过程并获得完全相同的结果。
如果用同一种编译器编译出可执行文件,并在同一种体系结构的机器上运行,并且使用一些平台相关的技巧,那么就有可能在不同的机器上进行浮点计算得到确定性的结果。
用 C 或 C++ 编写随机浮点计算代码并期望它在不同的编译器或不同架构的机器上产生完全相同的结果是非常简单的。
但如果你愿意付出大量努力,让你的编译器“严格”符合 IEEE 754 编译模型,并限制你使用的浮点运算集,你也许能够让不同的编译器和不同架构的机器与 IEEE 754 编译器兼容。浮点计算产生完全相同的结果。这通常会导致浮点计算性能显著降低。
如果您想讨论这个问题,或者想添加自己的细微差别,请发表评论!!我认为这个问题绝对没有得到解决,并希望了解其他人对浮点计算的精度和完美可重复性的经验。非常感兴趣。特别是如果您在现实世界中设法在不同的架构和编译器上获得二进制精确结果,请联系我。
以下是我迄今为止搜索到的一些资源。
我们向各种客户授权的技术基于确定性浮点计算(即使在 64 位条件下),并且自 2000 年以来一直以这种方式工作。
只要您坚持使用相同的编辑器和相同的 CPU 指令集,就可以使浮点计算完全确定。确切的实现将因平台而异(例如,在 x86、x64 和 PPC 平台上会有所不同)。
您必须确保内部精度设置为 64 位(而不是 80 位,因为只有 Intel 实现了这一点),并且舍入模式一致。此外,您必须在调用外部 DLL 后检查这一点,因为许多 DLL(Direct3D、打印机驱动程序、声音库等)可以更改精度或舍入模式,而无需将其改回。
该 ISA 符合 IEEE 标准,如果您的 x87 实现不符合 IEEE 标准,那么它根本就不是真正的 x87。
此外,您不能使用 SSE 或 SSE2 指令进行浮点计算,因为它们缺乏使其确定性的规范。”
来源:Jon Watte,论坛
“我在 GasPowered Games 工作,我可以第一时间告诉你,浮点计算是确定性的。你只需要相同的指令集和编译器,当然用户的处理器必须遵循 IEEE 754 标准。这个标准涵盖了我们所有的个人电脑和 360 游戏机。运行 DemiGod、Super Commander 1 和 Super Commander 2 的引擎也依赖于 IEEE754 标准。更不用说市场上所有支持点对点战斗的实时战略游戏也依赖于 IEEE754 标准。取决于 IEEE754 标准。只要你的游戏支持点对点战斗,每个客户端都需要广播他们在哪个更新期间执行了哪些操作,并依靠客户端计算机通过确定性浮点计算处理器来计算模拟/物理行为的细节。
当应用启动的时候我们调用如下代码:↓↓↓
_controlfp(_PC_24,_MCW_PC)_controlfp(_RC_NEAR,_MCW_RC)
此外,在每个时钟周期,我们都会断言以下浮点单元设置以确保它们没有改变:
gpAssert( (_controlfp(0,0) & _MCW_PC) == _PC_24 );gpAssert( (_controlfp(0, 0) & _MCW_RC) == _RC_NEAR );
有一些微软的 API 函数可能会改变浮点单元的模型,所以在调用这些微软的 API 函数之后,需要手动保证这些浮点单元模型的设置在不同的机器上仍然是一致的。断言是为了防止有人更改浮点单元的模型。
我们将编译器的浮点模型设置为 Fast /fp:fast(尽管这不是必需的)。
只要遵循 IEEE 标准,我们在装有各种 CPU(包括 AMD CPU 和 Intel CPU)的 PC 上使用这种方法从未遇到任何问题。我们的 Super Commander 或 Demigod 客户在他们的机器上没有遇到任何问题,我们这里谈论的是一款拥有超过一百万用户的游戏(Super Commander 1 + Expansion Pack)。如果游戏结束了,我们肯定会知道,因为重播或多人游戏无法正常工作。
我们在使用某些物理 API 时确实遇到了一些问题,因为它们的代码并非设计为确定性或完美可重现的。例如,某些物理 API 计算器在迭代过程中,迭代次数可能因 CPU 的速度而异,速度越快的 CPU 上的迭代次数可能越少。
以利亚,Gas Powered Games
如果存储玩家输入进行播放,那么存储的玩家输入将无法在具有不同CPU架构、编译器或不同优化选项的机器上正常播放。在MotoGP中,这意味着我们无法在Xbox和PC之间共享保存的重播文件。这也意味着如果我们在游戏的调试版本上保存重播文件网络游戏开发,重播文件将无法在Xbox和PC之间共享。它将无法在发布版本上正常工作,反之亦然。这在正常情况下可能不是问题(毕竟我们肯定不会发布调试版本),但如果我们要发布补丁,这就要求我们使用与原版游戏完全相同的编辑器来构建补丁。如果编译器在原版编译后已经升级,即我们使用较新的编译器来编译补丁,这可能会产生足够大的影响,导致原版保存的视频播放文件无法在新版本上正常播放。(如果不换编译器的话,这基本是不可能的)。
这太疯狂了!为什么我们不能让所有硬件都以同样的方式工作呢?好吧,我们可以,如果我们不关心性能的话。我们可以说,“嘿,我亲爱的硬件先生,忘掉你那些疯狂的乘法和加法指令吧,只使用最基本的 IEEE 实现”,以及“亲爱的编译器,请不要试图优化我们的代码来打扰我们”。这样,我们的程序就可以在不同的 CPU 架构、编译器不同、或者优化选项不同的机器上一致地运行,否则会非常慢。
Shawn Hargreaves,MSDNBlog
“Warzone 2 采用帧同步网络模型,这要求每个客户端都获得完全相同的结果,包括尾数的最低位。否则,不同机器上的模拟将开始出现分歧。很难在每个客户端上都获得完全一致的结果,这意味着我们只需要通过网络发送用户的输入信息。所有其他游戏状态都需要在本地计算。在此过程中,我们发现 AMD 和 Intel 处理器对某些数学函数的处理略有不同(这包括 sin、cos、tan 及其逆),因此我们不得不将它们包装在未优化的函数调用中,以强制编译器将它们的操作保持在单精度中。这足以使 AMD 和 Intel 处理器保持一致,但这绝对是一种只能学习的做法。
肯·米勒(Ken Miller),PandemicStudios
。。。在 FSW1 中,当检测到玩家不同步时,玩家会被“魔法狙击手”当场击毙。所有这些不同步的情况都在 FSW2 中得到了修复。我们使用精确的浮点数。在 PC 上我们使用 Havok 的浮点运算库,而不是 IMD。整数模数也是一个问题,因为 C++ 标准说这个操作是“实现定义的”(在这种情况下,多个编译器/平台将有不同的实现)。总的来说,我喜欢为帧同步开发的工具,它们使在 FSW2 的代码中找到不同步变得很容易。非常简单。
布拉尼米尔·卡拉季奇,Pandemic Studios
“我所知道的浮点运算不一致的主要原因有三个:↓↓↓
编译器对操作的优化。
复杂的指令,如乘法累积指令或正弦波指令。
有些指令特定于 x86 架构,在其他平台上不可用。
好消息是,大多数问题都来自第三个问题,而这或多或少可以自动解决。从决策角度(“我们应该投资确保浮点一致性,还是这只是浪费时间”),我会说这肯定不会是徒劳的,如果你能列举出从一致性中获得的实际好处,那么值得(持续)努力去实现这一点。
总结:使用 SSE2 或者 SSE。如果做不到,就配置浮点运算的 CSR 使用 64 位进行中间计算,避免使用 32 位浮点数。即使实际操作中只使用后一种方案,只要大家能理解这一点就好了。”
Yossi Kreinin,一致性:如何违背 IEEE 浮点数的目的
这个问题的简短回答是肯定的。浮点计算是完全确定性的,因为根据 IEEE 浮点标准,每个运算都有明确的规则,只要符合 IEEE 标准,浮点计算就是完全确定性的。确定性的,但这并不意味着浮点计算可以在不同的机器、编译器和操作系统上产生完全相同的可重现的结果。
这些问题的详细答案可能可以在 David Goldberg 的《每个计算机科学家都应该知道的浮点运算》中找到,这可能是关于浮点运算的最佳参考资料。转到 IEEE 标准部分,直接查看最关键的部分。
最后,如果您在相同的初始输入信息上运行相同顺序的浮点运算,则整体结果应该是完全可重现的。浮点运算的确切顺序可能因您的编译器/操作系统/标准库而异。这因系统而异,因此您可能会以这种方式遇到一些小错误。
浮点数学中经常遇到的一个问题是,如果你的方法数值不稳定,并且你输入浮点单元的输入大致相同但不完全相同。如果你知道浮点数的稳定性,你可以保证在一定误差范围内的可重复性。如果你想了解更多相关信息,可以阅读上面链接中 Goldberg 关于浮点算术的文章(即“每个计算机科学家都应该知道的浮点运算”)或查找关于数值分析的介绍性文档。
托德·甘布林(Todd Gamblin),StackOverflow
C++标准并没有为浮点类型float、double、longdouble指定二进制表示。虽然标准没有做出这个要求,但是大多数C++编译器在实现浮点运算时都遵循一个标准,即IEEE 754-1985标准,至少对于float和double类型是这样。这直接导致现代CPU的浮点运算单元也支持这个标准。IEEE 754标准规定了浮点数和浮点运算的二进制格式。但是,不同的编译器对IEEE 754全部特性的支持程度不同。这就导致程序员会遇到这样或那样的陷阱。
Günter Obiltschnig,C++ 中浮点运算的跨平台问题
浮点运算很大程度上依赖于浮点单元的硬件实现、编译器的具体实现以及编译器所做的优化,以及系统的数学库(libm),通过实验我们可以看到,可重复性基本上只有在使用相同的系统数学库和相同的编译器且设置相同的情况下才会出现。
STREFLOP 库
浮点编程的目标:↓↓↓
准确性——产生的结果非常“接近”正确结果。
可重复性 - 在不同时间运行相同的计算会产生一致的结果。在不同的构建配置上运行相同的计算会产生一致的结果。在不同的编译器上运行相同的计算会产生一致的结果。在不同的平台上运行相同的计算将产生一致的结果。
性能——尽可能生成最高效的代码。
这些选项经常发生冲突!明智地使用编译器选项可以让您控制权衡。
深入 C++ 编译器:浮点运算的一致性 %5Fconsistency.pdf。
如果严格的可重复性和一致性很重要,请不要更改浮点运行时设置,并且仅使用 fp-model strict(在 Linux 或 Mac OS 上)或 /fp:strict(在 Windows 上)。或者使用预编译宏 fenv_access。
深入的 C++ 编译器手册
在 fp:strict 模式下,编译器不会执行任何可能影响浮点运算准确性的优化。编译器会在赋值、类型转换和函数调用期间正确处理舍入,并对浮点数执行内部舍入运算。浮点算术单元的寄存器中始终使用相同的精度。默认情况下,浮点数的异常语义和对浮点单元环境设置的敏感性处于启用状态。某些优化(如收缩)被禁用,因为编译器无法保证这些优化在所有情况下都是正确的。
Microsoft Visual C++浮点优化 (VS.71).aspx#floapoint_topic4
需要注意的是,浮点计算的结果在 PowerPC 和 Intel 平台上可能并不完全相同,因为 PowerPC 上的标量和矢量浮点单元的核心是围绕同一单元的加法和乘法而设计的。芯片有单独的乘法器和加法器,这意味着这些操作必须单独执行。这意味着在计算的某些步骤中,Intel 的 CPU 可能会触发一些额外的舍入操作,这可能会在计算的乘法阶段引入大约一半的舍入误差。
Apple 开发者支持
对于所有属于 IEEE 操作的指令(*、+、-、/、平方根、比较,无论它们是 SSE 指令还是 x87 指令),它们将在相同的控制设置(相同的精度控制和舍入模式、0 截断等)和输入下在所有平台上产生完全相同的结果。这对于 32 位或 64 位处理器都是如此。。。在 x87 指令集中,哪些超越指令(例如 fsin、fcos 等)可能会根据实现方式产生略有不同的结果。它们指定了保证的相对误差范围,但并不完全准确。
英特尔软件网络支持
我的担忧来自于 IEEE-754 在不同硬件上的实现差异。我已经知道编程语言存在问题,在源代码级别的实现和在汇编级别的实现会带来细微的差异。(Mon08)现在,我更感兴趣的是 Intel/SSE 和 PowerPC 在指令级别的差异。
IEEE 754邮件列表上的D. Monniaux
如果您想要一致性……您必须避免使用非 754 指令,即使它们变得越来越普遍。例如,连续两次取平方根的倒数将无法正确舍入。,甚至不同的实现也可能不会产生一致的结果。此外,AMD 和 Intel 平台上 x87 超越指令的实现也不同。
David Hough 在 754 IEEE 邮件列表中
是的,有可能获得可重复的结果。但是,如果不定义编程方法,就无法做到这一点。而且其后果比任何支持者承认的都要严重得多 - 特别是,它实际上无法与大多数并行性有效兼容。
Nick Maclaren 在 754 IEEE 邮件列表中
如果谈论的是可行性,那么事情就大不相同了,在实际项目中不太可能期待可重复的结果。我们之前已经为这个目标努力过,我希望我们不必再为此努力。
Nick Maclaren 在 754 IEEE 邮件列表中
IEEE 754-1985 允许许多实现变体(例如对某些值的编码和对某些异常的检测)。IEEE754-2008 收紧了许多领域,但仍然存在一些实现变体(尤其是二进制格式)。可重复性条款建议语言标准应提供编写可重复程序的方法(即如何编写程序,使它们在编程语言的所有实现上产生相同的结果),并描述需要做什么才能实现可重复的结果。
IEEE754-2008 标准的维基百科页面
#可重复性
如果您想要在最近值模式下关于单精度或双精度计算(包括溢出和下溢条件下的计算)的语义几乎完全忠实于严格的 IEEE-754,则可以使用精度和选项。实现有些受限,并且编程风格强制在两次浮点运算之间将操作数写入内存。这将导致一些性能损失。此外,当发生下溢时,双精度浮点数会被四舍五入,会有细微的差异。
对于当前的个人计算机,更简单的解决方案是强制编译器使用 SSE 单元进行 IEEE-754 类型的计算。但是,大多数嵌入式系统使用 IA32 微处理器或微控制器,而不是配备 IA32 的微处理器。该单元的处理器。
David Monniaux,验证浮点计算的陷阱
6.重复性---
即使在 1985 版 IEEE-754 标准下,如果标准的两个实现对同一数据执行相同的操作,使用相同的舍入模式和默认异常处理,操作的结果也将是相同的。新标准试图进一步描述如何让程序在不同的实现上产生相同的浮点运算结果。标准中描述的操作都是可重复的操作。
诸如库函数或归约操作之类的推荐操作不可重现,因为它们不是所有实现都需要的。出于同样的原因,对下溢和不精确表示法的依赖也是不可重现的。因为下溢有两种不同的方法,主要是为了保持 IEEE-754 (1985) 和 IEEE-754 (2008) 之间的一致性。舍入模式是可重现的。可选属性不可重复。
如果想要可重现,则应避免使用改变值的优化,包括使用结合性和分配性,以及在程序员未明确使用运算符时融合乘法和加法。
Peter Markstein,《IEEE 浮点运算新标准》
不幸的是,IEEE 标准并不保证同一程序在满足系统要求的所有平台上都会产生相同的结果。由于各种原因,大多数程序在不同的系统上会产生不同的结果。首先,大多数程序涉及数字的十进制和二进制表示之间的转换,而 IEEE 标准并没有完全指定执行此类转换必须达到的精度。另一方面,许多程序使用系统库提供的基本功能,而标准并没有完全指定执行此类转换必须达到的精度。当然,大多数程序员都知道这些功能超出了 IEEE 标准的范围。
许多程序员可能没有意识到,即使一个程序只使用 IEEE 标准规定的数字格式和操作,在不同的系统上也会产生不同的结果。事实上,标准的作者们本意是允许不同的实现产生不同的结果。他们的意图在 IEEE 754 标准中对术语“目的”的定义中显而易见:“目的可以由用户明确指定,也可以由系统隐式提供(例如,表达式的中间结果或过程的参数)。有些语言将中间计算的结果放入目的地,这是用户无法控制的。尽管如此,本标准还是将操作的结果定义为目的地的格式和操作数的值。”(IEEE 754 - 1985,第 7 页)。换句话说,IEEE 标准要求将每个结果正确地舍入到目的地的精度并放入目的地,但标准并不要求目的地的精度由用户的程序决定。 因此,不同的系统可能会将其计算的结果放入具有不同精度的目的地网络游戏开发,这可能会导致同一个程序产生不同的结果(有时这可能非常显著),即使这些系统都符合标准。
IEEE 754 实现之间的差异
#3098