跨平台浮点数一致性

19

我正在开发一个跨平台的网络游戏,使用锁步模型进行玩耍。简而言之,这意味着只通信输入,并且所有游戏逻辑在每个客户端的计算机上模拟。因此,一致性和确定性非常重要。

我正在使用MinGW32编译Windows版本,其中使用GCC 4.8.1,并且在Linux上使用GCC 4.8.2进行编译。

最近给我留下深刻印象的是,当我的Linux版本连接到Windows版本时,即使相同的代码在两台计算机上都编译了,程序也会立即出现分歧或不同步。原来问题在于Linux版本是64位编译的,而Windows版本是32位的。

编译一个Linux的32位版本后,我终于松了一口气,问题得以解决。但是,这让我开始思考和研究浮点数确定性。

以下是我的研究:

如果程序满足以下条件,那么它通常是一致的:

  • 在相同的架构上运行
  • 使用相同的编译器进行编译

因此,如果我假设针对PC市场,每个人都有x86处理器,那么这解决了第一个要求。但是,第二个要求似乎有点荒唐。

MinGW、GCC和Clang(分别在Windows、Linux和Mac上)都是基于/兼容GCC的不同编译器。这是否意味着无法实现跨平台确定性?或者仅适用于Visual C++与GCC之间?

另外,在这种确定性中,优化标志-O1或-O2是否会产生影响?将其关闭是否更安全?

最终,我有三个问题要问:

  • 1)使用MinGW、GCC和Clang作为编译器时,是否可以实现跨平台确定性?
  • 2)这些编译器应该设置哪些标记以确保操作系统/CPU之间的最大一致性?
  • 3) 浮点数的精度对我来说并不那么重要,重要的是它们是一致的。 有没有一种方法可以将浮点数降低到较低的精度(比如3-4位小数),以确保跨系统的小舍入误差变得不存在? (迄今为止我尝试编写的每个实现都失败了)

编辑:我进行了一些跨平台实验。

使用浮点数表示速度和位置,我在Linux Intel笔记本电脑和Windows AMD台式机上保持了最多15位小数的浮点值同步。 但两个系统都是x86_64。 虽然测试很简单--只是通过网络移动实体,试图确定任何可见错误。

如果一个x86计算机连接到x86_64计算机,是否假设将产生相同的结果? (32位与64位操作系统)


3
我认为不同的优化标志可能会导致您的模拟不一致,因为编译器有可能选择生成不同的公式和计算方式来得出相同的结果(特别是在优化大小和速度时)。此外,运行时 CPU 浮点标志的舍入模式和错误处理模式也会影响结果(编译器有时会生成代码来设置这些标志而不告知您)。然而,我并不是专家,以上仅供参考。 - yzt
2
对于第三个问题,你应该研究“定点算术”。这意味着你基本上将所有数字乘以一个固定的值(例如1000或65536或其他值;可以将其视为使用毫米和毫秒而不是米和秒进行计算),并使用整数变量和值进行所有计算。但是,你应该非常小心“数值稳定性”和误差累积以及误差边界。定点数可以实现相当高效,并且使它们确定性更容易得多。 - yzt
1
所以固定点算术基本上是将普通的整数进行扩大,然后在使用该值时再进行缩小吗? - BWG
1
@BWG:不完全是,但离正确也不远了。您可以阅读定点算术的维基百科文章 - yzt
1
你可能会发现这个链接有用:http://randomascii.wordpress.com/2013/07/16/floating-point-determinism/ - John Bartholomew
显示剩余5条评论
4个回答

20
当然,跨平台和交叉编译器的一致性是可能的。只要有足够的知识和时间,任何事情都是可能的!但这可能非常困难、耗时或者不切实际。
以下是我可以预见到的问题(无特定顺序):
1. 请记住,即使是一个极小的误差,例如 plus-or-minus 1/10^15,也会被放大成显著的数字(将该数字乘以误差边界再乘以十亿,现在你就有了 plus-or-minus 0.000001 的误差,这可能是显著的)。这些误差会随着时间的推移,在许多帧中累积,直到产生不同步的模拟。或者它们可能在比较值时显现出来(即使是浮点数比较中天真地使用 "epsilons" 也可能无助;只能把它们位移或推迟显现)。
2. 上述问题并不是分布式确定性模拟(像你的模拟)所特有的。它触及了 "数值稳定性" 这个难以处理且经常被忽视的问题。
3. 不同的编译器优化开关和不同的浮点行为决策开关可能会导致编译器为相同语句生成略微不同的 CPU 指令序列。显然,这些编译必须在使用完全相同的编译器编译时是相同的,或者生成的代码必须进行严格的比较和验证。
4. 32 位和 64 位程序(注意:我说的是程序而不是 CPU)可能会展示略微不同的浮点行为。默认情况下,除非你在编译器命令行中指定(或者在代码中使用内联汇编指令),否则 32 位程序不能依赖于比 x87 指令集更高级的任何东西(没有 SSE、SSE2、AVX 等等)。另一方面,64 位程序保证在支持 SSE2 的 CPU 上运行,因此编译器将默认使用这些指令(再次强调,除非被用户覆盖)。虽然 x87 和 SSE2 浮点数据类型及其操作类似,但它们 - 据我所知 - 不完全相同。这会导致模拟中的不一致性,如果一个程序使用其中一个指令集,而另一个程序使用另一个指令集的话。
5. x87 指令集包括一个 "控制字" 寄存器,其中包含控制某些浮点操作的标志(例如精确舍入行为等)。这是运行时的事情,你的程序可以做一组计算,然后更改这个寄存器,再做完全相同的计算并得到不同的结果。显然,这个寄存器必须在不同的机器上进行检查、处理和保持一致。编译器(或程序中使用的库)可以生成在程序之间不一致地在运行时更改这些标志的代码。
6.

在 x87 指令集中,英特尔和 AMD 历史上实现方式有些不同。例如,一个供应商的 CPU 内部计算使用更多的位数(因此可以得出更精确的结果),而另一个供应商则可能不同,这意味着如果您在来自两个不同供应商的不同 CPU 上运行(都是 x86 架构),那么简单计算的结果 可能 不相同。我不知道这些更高精度的计算如何以及在什么情况下启用,无论是在正常操作条件下发生还是必须专门请求,但我知道这些差异确实存在。

  • 随机数及其在程序中的一致性和确定性没有任何关系。它很重要并且是许多错误的来源,但最终只是需要保持同步的状态数据的几个附加位。

  • 以下是一些可能有所帮助的技术:

  • 一些项目使用 "定点" 数字和定点算法来避免浮点数舍入误差和不可预测性。请参阅维基百科文章以获取更多信息和外部链接。

  • 在我自己的一个项目中,在开发过程中,我习惯于对所有游戏实例的相关状态(包括许多浮点数)进行哈希处理,并每帧通过网络发送哈希以确保在不同机器上没有任何一个状态位不同。这也有助于调试,因为我不需要依靠眼睛来看出存在哪些不一致性(这样也无法告诉我它们的起源),而是会知道某个游戏状态部分在一个机器上开始发生偏差,并且准确地知道它是什么(如果哈希检查失败,我将停止模拟并开始比较整个状态)。
    该功能从代码库的开始就被实现,并且仅在开发过程中用于调试(因为它具有性能和内存成本)。

  • 更新(针对下面的第一个评论):如我在第1点中所说,其他答案中的其他人也说过,那并不能保证什么。如果您这样做,您可能会降低出现不一致性的概率和频率,但可能性不会变为零。如果您不仔细和系统地分析代码中发生的情况以及可能的问题来源,那么无论您如何“四舍五入”您的数字,仍然可能会遇到错误。

    例如,假设你有两个数字(例如作为两个计算结果),它们应该产生相同的结果,分别为1.111499999和1.111500001,如果你将它们舍入到小数点后三位,它们变成了1.111和1.112。原始数字之间的差异仅为2E-9,但现在已经变成了1E-3。实际上,您已经将错误增加了500,000倍。即使进行了舍入,它们仍然不相等。这只会加剧问题。
    诚然,这种情况并不经常发生,我给出的示例是两个不幸的数字,但仍有可能遇到这些数字。当你遇到这种情况时,就会遇到麻烦。唯一确保没有问题的解决方案,即使你使用固定点算法或其他算法,也是对所有可能出现问题的区域进行严格而系统的数学分析,并证明它们将在程序中保持一致。
    除此之外,在我们这些凡人看来,你需要有一种完全可靠的方法来监控情况,并找出最微小的差异何时以及如何发生,以便能够事后解决问题(而不是依赖于肉眼来观察游戏动画或对象移动或物理行为中的问题)。

    谢谢你的回答。我已经阅读了一些关于定点算术的内容,但我想把它作为最后的选择。如果我将所有浮点数四舍五入到三位小数(使用类似于* 1000、floor、/ 1000的方法),那么这样做是否会解决计算中的不一致性呢?感谢哈希的想法。我可能会写类似的东西。 - Izman
    1
    @lzman:是的,那样做太天真了,而且速度相对较慢。(四舍五入为二进制会更有意义) - MSalters

    1
    1. 实际上不行。例如,sin() 可能来自库或编译器内置函数,并且在舍入方面有所不同。当然,这只有一个比特,但已经不同步了。而且这个一位误差可能随着时间的推移累积,因此即使是不精确的比较也可能不足够。
    2. 您不能为给定类型减少FP精度,我甚至不知道它如何帮助您。您将偶尔出现的1E-6差异变成偶尔出现的1E-4差异。

    1
    我知道三角函数在跨平台上不是确定性的(如果我没记错的话,有一个开发者将结果四舍五入到一位小数以保持一致性)。另一个我听说过的大问题是rand(),但除了三角和随机函数之外,我不会使用任何复杂的数学。那么,如果我仅限于使用这些函数,我还会遇到麻烦吗?看起来似乎没有解决方案。 - Izman
    1
    在IEEE 754系统上(x86通常是这样的),+-*/sqrt是确定性的,但需要注意的是_舍入方向很重要_。随机数实际上并不是一个问题,新的<random>提供了确定性PRNG。 - MSalters
    谢谢,这正是我想听到的。 - Izman

    1

    除了你对决定论的担忧之外,我还有另一个观点:如果你担心分布式系统的计算一致性,那么可能存在设计问题。

    你可以将应用程序视为一堆节点,每个节点负责自己的计算。如果需要其他节点的信息,则该节点应将其发送给您。


    1

    1.) 原则上,跨平台、操作系统和硬件兼容性是可能的,但实际上很麻烦。

    一般来说,您的结果将取决于您使用的操作系统、编译器和硬件。更改其中任何一个,您的结果可能会改变。您必须测试所有更改。我使用Qt Creator和qmake(cmake可能更好,但qmake适合我),并在Windows上的MSVC、Linux上的GCC和Windows上的MinGW-w64中测试我的代码。我测试32位和64位版本。每当代码发生更改时都必须这样做。

    2.) 和 3.) 在浮点数方面,一些编译器在32位模式下使用x87而不是SSE。当发生这种情况时,可以看到其后果,例如为什么数字计算程序在进入NaN时开始运行得更慢? 所有64位系统都具有SSE,因此我认为大多数情况下在64位上使用SSE/AVX,否则,在32位模式下,您可能需要使用类似-mfpmath = sse和-msse2的东西来强制使用SSE。

    但如果您想在Windows上获得更兼容的GCC版本,则应使用32位的MingGW-w64(也称为MinGW-w32)或64位的MinGW-w64。这与MinGW(也称为mingw32)不是同一件事。这些项目已经分道扬镳。MinGW依赖于MSVCRT(MSVC C运行时库),而MinGW-w64则不依赖。Qt项目对MinGW-w64和安装有很好的描述。http://qt-project.org/wiki/MinGW-64-bit 您还可以考虑编写一个CPU分发器用于AVX和SSE的Visual Studio CPU分发器

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接