跨平台浮点一致性

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

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

最近让我感到震惊的是,当我的Linux版本连接到我的Windows版本时,即使在两台机器上都编译了相同的代码,程序也会立即发散或者解除同步! 原来问题是Linux版本是通过64位编译的,而Windows版本是32位的。

编译Linux 32位版本后,我很高兴地解决了这个问题。 然而,这让我思考和研究浮点确定性。

这是我所收集的:

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

  • 跑在同一架构上
  • 使用相同的编译器编译

所以如果我假设,针对个人电脑市场,每个人都有一个x86处理器,那么解决了要求之一。 但是,第二个要求似乎有点愚蠢。

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

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

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

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

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

使用漂浮点的速度和位置,我保持一个Linux英特尔笔记本电脑和一个Windows AMD桌面计算机同步浮点值的小数点后15位。 但是这两个系统都是x86_64。 testing很简单 – 只是通过networking移动实体,试图确定任何可见的错误。

如果x86计算机连接到x86_64计算机,那么假设相同的结果是有效的呢? (32位与64位操作系统)

Solutions Collecting From Web of "跨平台浮点一致性"

跨平台和交叉编译器的一致性当然是可能的。 任何事情都有可能给予足够的知识和时间! 但是这可能非常困难,或者非常耗时,或者实际上是不切实际的。

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

  1. 请记住,即使是一个非常小的正负1/10 ^ 15的误差也会变得显着(把这个数乘以10亿的误差,现在你有一个正负0.000001的误差可能是重要的)。这些错误可能会随着时间的推移累积,直到你有一个不同步的模拟。 或者,当你比较价值时,他们可以表现出来(甚至天真地使用浮点比较中的“epsilon”可能无助;只能取代或延迟表现形式)。

  2. 上述问题并不是独立于分布式确定性模拟(就像你自己的)。关于“ 数值稳定性 ”的问题,这是一个困难而且经常被忽视的问题。

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

  4. 32位和64位程序(注:我在说程序而不是CPU)可能会表现出略有不同的浮点行为。 默认情况下,除非在编译器命令行中指定了这一点,否则32位程序不能依赖比CPU的x87指令集更高级的任何内容(无SSE,SSE2,AVX等)(或者在内核中使用内联/内联汇编指令)另一方面,一个64位程序保证在支持SSE2的CPU上运行,所以编译器将默认使用这些指令(同样,除非用户重写)。而x87和SSE2浮点数据类型和他们的操作是相似的,他们是 – AFAIK – 不完全相同。 如果一个程序使用一个指令集而另一个程序使用另一个指令集,这将导致模拟中的不一致。

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

  6. 同样,在x87指令集的情况下,英特尔和AMD在历史上实现的东西稍有不同。 例如,一个供应商的CPU可能会在内部使用更多位进行一些计算(因此得出更准确的结果),这意味着如果碰巧在两个不同的供应商的两个不同的CPU(两个x86)上运行,简单计算的结果可能不一样。 我不知道如何以及在什么情况下能够进行更高精度的计算,以及这些计算是否在正常的操作条件下发生,或者您必须特别要求,但是我知道存在这些差异。

  7. 随机数和在程序间一致和确定地生成它们与浮点一致性无关。 这是很多错误的重要来源,但最终还是需要保持同步。

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

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

  2. 在我自己的一个项目中,在开发过程中,我曾经把所有的相关状态(包括大量的浮点数)散列到游戏的所有实例中,并在每一帧中通过网络发送散列,以确保一个位那个状态在不同的机器上没有什么不同。 这也有助于调试,而不是相信我的眼睛,看看什么时候和哪里出现不一致的地方(反正他们不知道它们来自哪里),我会知道一个机器上游戏状态的某些部分开始发散从其他人那里,并确切知道它是什么(如果哈希检查失败,我会停止模拟,并开始比较整个状态)。
    这个特性从一开始就在代码库中实现,并且只在开发过程中用于帮助调试(因为它具有性能和内存成本)。

更新 (回答下面的第一条评论):正如我在第1点所说的,其他人在其他答案中所说的,这并不能保证任何东西。 如果你这样做,你可能会减少发生不一致的概率和频率,但可能性不会变成零。 如果您没有仔细分析代码中发生的情况以及可能的问题根源,无论您的数字“四舍五入”多少,仍然有可能发生错误。

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

诚然,这种情况并没有发生,我给出的例子是两个不幸的数字来解决这个问题,但是仍然有可能找到这些数字。 而当你这样做,你遇到了麻烦。 唯一可靠的解决方案,即使使用定点算术或其他方法,也是对所有可能的问题区域进行严格和系统的数学分析,证明它们在各个程序之间保持一致。

简而言之,对于我们这些凡人来说,你需要有一个水密的方式来监控情况,并且确切地知道何时以及如何发生最微小的差异,以便事后能够解决问题(而不是依靠你的眼睛看游戏动画或对象移动或物理行为中的问题。)

  1. 不,不是在实践中。 例如, sin()可能来自库或编译器本身,并且在舍入方面有所不同。 当然,这只是一点,但已经不同步。 而且这一点误差可能会随着时间而增加,所以即使是不精确的比较也可能是不够的。
  2. N / A
  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。 把这看作是这种情况发生的后果的一个例子为什么一个数字运算程序开始运行速度要慢得多? 所有64位系统都有SSE,所以我认为大多数情况下使用64位的SSE / AVX,例如在32位模式下,可能需要强制SSE使用-mfpmath=sse and -msse2

但是如果你想在Windows上使用更兼容的GCC版本,那么我将使用32位的MinGW-w64(又名MinGW-w32)或64位的MinGW-w64。 这与MinGW (aka mingw32) 不是一回事 。 这些项目已经分化了。 MinGW依赖于MSVCRT (MSVC C运行时库),而MinGW-w64则不依赖。 Qt项目对MinGW-w64和安装有很好的描述。 http://qt-project.org/wiki/MinGW-64-bit

您可能还想考虑为AVX和SSE的Visual Studio编写一个CPU调度程序cpu调度程序 。