【问题标题】:Cross Platform Floating Point Consistency跨平台浮点一致性
【发布时间】:2014-01-24 15:37:25
【问题描述】:

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

我正在使用 GCC 4.8.1 的 MinGW32 上编译 Windows 版本,而在 Linux 上我正在使用 GCC 4.8.2 进行编译。

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

在编译了一个 32 位的 Linux 版本后,我很庆幸问题得到了解决。然而,它让我开始思考和研究浮点确定性。

这是我收集到的:

一个程序通常是一致的,如果它是:

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

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

MinGW、GCC 和 Clang(分别是 Windows、Linux、Mac)都是基于/兼容/基于 GCC 的不同编译器。这是否意味着不可能实现跨平台确定性?还是仅适用于 Visual C++ vs GCC?

同样,优化标志 -O1 或 -O2 会影响这种确定性吗?离开它们会更安全吗?

最后,我要问三个问题:

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

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

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

如果 x86 计算机连接到 x86_64 计算机,假设相同的结果会成立是否有意义? (32 位与 64 位操作系统)

【问题讨论】:

  • 我认为不同的优化标志会使您的模拟不一致,因为编译器可能选择生成不同的公式和计算以达到相同的结果(尤其是在针对大小与. 速度。)还有用于舍入模式和错误处理模式的运行时 CPU 浮点标志(编译器有时会在您不知情的情况下生成代码来设置这些标志。)但是,我绝不是这方面的专家,所以......
  • 第三个问题,你应该研究“定点算法”。这意味着您基本上将所有数字乘以一个固定值(例如 1000 或 65536 或其他;将其视为使用毫米和毫秒而不是米和秒),并使用整数变量和值进行所有计算。但是,您应该非常小心“数值稳定性”以及错误累积和错误界限。定点数可以非常有效地实现,并且使它们具有确定性要容易得多。
  • 所以定点算术基本上就是对一个普通的int进行膨胀,然后在使用值的时候再放气?
  • 我支持使用定点算法的建议;以我的经验,让不同的编译器(甚至同一台机器上的不同版本的相同编译器)为浮点运算返回相同的结果几乎是不可能的。
  • @BWG:不完全是,但这也不是太离谱。 You can read the Wikipedia article of fixed-point arithmetic.

标签: c++ linux gcc


【解决方案1】:
  1. 不,实际上没有。例如,sin() 可能来自库或编译器内在函数,并且舍入不同。当然,这只是一点点,但这已经不同步了。随着时间的推移,这一位错误可能会累积起来,因此即使是不精确的比较也可能不够。
  2. 不适用
  3. 您无法降低给定类型的 FP 精度,我什至看不出它对您有何帮助。您会将偶尔的 1E-6 差异转变为偶尔的 1E-4 差异。

【讨论】:

  • 我知道三角函数不是确定性的跨平台(iifc,一位开发人员将结果四舍五入到小数点后 1 位以保持一致性)。我听说过的另一个大问题是 rand(),但除了三角函数和随机函数之外,我不会使用任何复杂的数学。如果我只限于这些,我还会有麻烦吗?好像没有办法解决
  • 在 IEEE 754 系统(通常是 x86)上,+-*/sqrt 是确定性的,但条件是舍入方向很重要。随机实际上不是问题,新的<random> 提供确定性 PRNG。
  • 谢谢,这正是我想听到的
【解决方案2】:

除了您对确定性的担忧之外,我还有一点要说:如果您担心分布式系统上的计算一致性,那么您可能遇到了设计问题。

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

【讨论】:

    【解决方案3】:

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

    以下是我可以预见的问题,不分先后:

    1. 请记住,即使是正负 1/10^15 的极小误差也可能会变得很重要(您将该数字乘以该误差幅度乘以 10 亿,现在您就有了一个正负或减去 0.000001 错误,这可能很重要。)这些错误会随着时间的推移累积,在许多帧上,直到你有一个不同步的模拟。或者它们可以在您比较值时显现(即使在浮点比较中天真地使用“epsilons”也可能无济于事;只会取代或延迟显现。)

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

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

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

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

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

    7. 随机数以及跨程序一致且确定地生成它们与浮点一致性无关。它很重要,也是许多 bug 的来源,但最终你必须保持同步的只是一些状态。

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

    1. 一些项目使用“定点”数字和定点算法来避免舍入错误和浮点数的一般不可预测性。 Read the Wikipedia article 了解更多信息和外部链接。

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

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

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

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

    除此之外,对于我们这些凡人来说,您需要有一种严密的方式来监控情况并准确找出最轻微的差异发生的时间和方式,以便能够在事后解决问题(而不是依赖在你的眼睛上看到游戏动画或物体运动或物理行为中的问题。)

    【讨论】:

    • 感谢您的回答。我读了一些关于定点算术的书,但我想把它作为最后的手段。说如果我将所有浮点数四舍五入到小数点后 3 位(使用 * 1000、下限、/1000 之类的东西)就能解决计算中的不一致问题,我是否天真?不过,感谢您的散列想法。我可能会写一些类似的东西。
    • @lzman:是的,这太天真了,而且行不通。此外,它相当缓慢。 (四舍五入到二进制会更有意义)
    【解决方案4】:

    1.) 原则上跨平台、操作系统、硬件兼容性是可能的,但在实践中却很痛苦。

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

    2.) 和 3.) 在浮点方面,一些编译器将在 32 位模式下使用 x87 而不是 SSE。将此视为发生这种情况时的后果的示例Why a number crunching program starts running much slower when diverges into NaNs? 所有 64 位系统都有 SSE,所以我认为大多数人在其他情况下使用 64 位的 SSE/AVX,例如在 32 位模式下,您可能需要使用 -mfpmath=sse and -msse2 之类的东西强制 SSE。

    但是,如果您想在 Windows 上使用更兼容的 GCC 版本,那么我会使用 MingGW-w64 用于 32 位(又名 MinGW-w32)或 MinGW-w64 用于 64 位。这是not the same thing as MinGW(又名mingw32)。项目出现了分歧。 MinGW 依赖于MSVCRT(MSVC C 运行时库),而 MinGW-w64 不依赖。 Qt 项目对 MinGW-w64 和安装有很好的描述。 http://qt-project.org/wiki/MinGW-64-bit

    您可能还想考虑编写一个 CPU 调度程序 cpu dispatcher for visual studio for AVX and SSE

    【讨论】:

      猜你喜欢
      • 2016-01-15
      • 2015-04-07
      • 2010-10-29
      • 1970-01-01
      • 1970-01-01
      • 2019-08-13
      • 1970-01-01
      • 1970-01-01
      • 2013-04-01
      相关资源
      最近更新 更多