跨平台浮点一致性

Cross Platform Floating Point Consistency

本文关键字:一致性 跨平台      更新时间:2023-10-16

我正在开发一款跨平台游戏,该游戏使用锁步模型在网络上玩。作为一个简单的概述,这意味着只有输入被通信,所有游戏逻辑都在每个客户的计算机上模拟。因此,一致性和决定论是非常重要的。

我在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/与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位操作系统)

跨平台和跨编译器的一致性当然是可能的。只要有足够的知识和时间,一切皆有可能!但这可能非常困难,或者非常耗时,或者实际上不切实际。

以下是我可以预见的问题,没有特别的顺序:

  1. 请记住,即使是正负1/10^15的极小误差也可能会变得显著(你将这个数字与误差幅度乘以10亿,现在你有一个正负0.000001的误差,这可能是显著的。)这些误差可能会随着时间的推移,在许多帧上积累,直到你进行了去同步模拟。或者,它们可以在比较值时表现出来(即使在浮点比较中天真地使用"epsilons"也可能没有帮助;只会取代或延迟表现出来。)

  2. 上述问题并非分布式确定性模拟所独有(如您的问题)。触及">数值稳定性"问题,这是一个困难且经常被忽视的主题。

  3. 不同的编译器优化开关和不同的浮点行为确定开关可能会导致编译器为相同的语句生成稍微不同的CPU指令序列。显然,使用完全相同的编译器,这些编译必须是相同的,或者生成的代码必须经过严格的比较和验证。

  4. 32位和64位程序(注意:我说的是程序而不是CPU)可能会表现出稍微不同的浮点行为。默认情况下,32位程序不能依赖CPU中x87指令集以外的任何高级指令集(没有SSE、SSE2、AVX等),除非您在编译器命令行上指定了这一点(或在代码中使用intrinsics/inline汇编指令)。另一方面,64位程序保证在支持SSE2的CPU上运行,因此编译器将在默认情况下使用这些指令(同样,除非用户重写。)虽然x87和SSE2浮点数据类型及其操作相似,但它们-AFAIK并不相同。如果一个程序使用一个指令集,而另一个程序则使用另一个,这将导致模拟中的不一致。

  5. x87指令集包括一个"控制字"寄存器,其中包含控制浮点运算某些方面的标志(例如,精确舍入行为等)。这是运行时的事情,您的程序可以进行一组计算,然后更改此寄存器,然后进行完全相同的计算,得到不同的结果。显然,必须对该寄存器进行检查和处理,并在不同的机器上保持相同。编译器(或您在程序中使用的库)可能会生成在运行时在程序之间不一致地更改这些标志的代码。

  6. 同样,在x87指令集的情况下,英特尔和AMD在历史上的实现方式略有不同。例如,一个供应商的CPU可能在内部使用比另一个更多的位进行一些计算(因此得出更准确的结果),这意味着如果您碰巧在两个不同供应商的两个不同CPU(均为x86)上运行,则简单计算的结果可能不相同。我不知道这些更高精度的计算是如何实现的,在什么情况下实现的,也不知道它们是在正常操作条件下发生的,还是你必须具体要求它们,但我知道这些差异是存在的。

  7. 随机数和在程序之间一致且确定地生成它们与浮点一致性无关。它很重要,也是许多错误的来源,但最终它只是你必须保持同步的状态的一小部分。

这里有一些技术可能会有所帮助:

  1. 一些项目使用">定点"数字和定点算术,以避免舍入错误和浮点数的一般不可预测性。阅读维基百科的文章,了解更多信息和外部链接。

  2. 在我自己的一个项目中,在开发过程中,我曾经对游戏的所有实例中的所有相关状态(包括许多浮点数)进行散列,并在每帧都通过网络发送散列,以确保在不同的机器上,即使是该状态的一位也没有不同。这也有助于调试,在调试中,我不会相信我的眼睛能看到何时何地存在不一致(无论如何,这不会告诉我它们起源于哪里),而是会知道一台机器上游戏状态的某个部分开始与其他部分产生分歧的那一刻,并确切地知道它是什么(如果哈希检查失败,我会停止模拟并开始比较整个状态。)
    此功能从一开始就在该代码库中实现,并且仅在开发过程中用于帮助调试(因为它具有性能和内存成本)

更新(回答下面的第一条评论):正如我在第1点中所说,以及其他人在其他回答中所说的,这并不能保证什么。如果这样做,可能会降低不一致发生的概率和频率,但可能性不会变为零。如果你不仔细系统地分析代码中发生的事情以及可能的问题来源,那么无论你对数字进行多少"四舍五入",都有可能出现错误。

例如,如果您有两个数字(例如,作为两次计算的结果,本应产生相同的结果),分别为1.1114999999和1.111500001,并将它们四舍五入到小数点后三位,则它们分别变为1.111和1.112。最初的数字只有2E-9,但现在变成了1E-3。事实上,您已经将错误增加了500'000倍。即使四舍五入,它们仍然不相等。你加剧了问题。

的确,这种情况很少发生,我举的例子是在这种情况下得到的两个不幸的数字,但仍然有可能发现自己有这些数字。当你这样做的时候,你就有麻烦了。即使你使用定点算术或其他方法,唯一可靠的解决方案是对所有可能的问题领域进行严格而系统的数学分析,并证明它们在各个程序中保持一致。

除此之外,对于我们这些凡人来说,你需要有一种严密的方法来监控情况,准确地发现最细微的差异发生的时间和方式,以便能够在事后解决问题(而不是依靠你的眼睛来观察游戏动画、物体移动或身体行为中的问题)。

  1. 没有,在实践中没有。例如,sin()可能来自库或编译器内部,并且在舍入方面有所不同。当然,这只是一个比特,但已经不同步了。随着时间的推移,一个比特的错误可能会累积起来,所以即使是不精确的比较也可能不够
  2. 不适用
  3. 你不能降低给定类型的FP精度,我甚至看不出它会对你有什么帮助。你会把偶尔的1E-6差异变成偶尔的1E-4差异

除了你对确定性的担忧之外,我还有一句话:如果你担心分布式系统上的计算一致性,你可能会遇到设计问题。

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

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 and -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

您可能还想考虑为visualstudio的AVX和SSE编写一个CPU调度器CPU调度器。