我正在开发一种跨平台游戏,该游戏使用锁步模型在网络上播放。简要概述一下,这意味着只传达输入信息,并且在每个客户端的计算机上模拟所有游戏逻辑。因此,一致性和确定性非常重要。
我在使用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 ++与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/10 ^ 15的误差也可能会变得很大(将这个数字乘以十亿的误差幅度,现在您将有正负0.000001的误差,这些错误可能会随着时间,在许多帧上累积,直到您出现不同步的仿真为止。或者它们可以在您比较值时显示(即使在浮点比较中天真地使用“ epsilons”也可能无济于事;仅替换或延迟显示)。
上面的问题并不是分布式确定性仿真所独有的(像您一样)。关于“ 数值稳定性 ” 的问题,这是一个困难且经常被忽略的主题。
不同的编译器优化开关和不同的浮点行为确定开关可能导致编译器为相同的语句生成稍微不同的CPU指令序列。显然,在所有编译中,使用完全相同的编译器,它们必须相同,或者必须严格比较和验证生成的代码。
32位和64位程序(注意:我是说程序而不是CPU)可能会表现出稍微不同的浮点行为。默认情况下,除非您在编译器命令行上指定(或使用以下命令中的内在函数/内联汇编指令),否则32位程序不能依赖比CPU的x87指令集更高级的内容(无SSE,SSE2,AVX等)。另一方面,可以保证64位程序可以在具有SSE2支持的CPU上运行,因此,编译器将默认使用这些指令(同样,除非被用户覆盖)。x87和SSE2浮点数据类型时。并且它们的操作类似,它们是-AFAIK-不相同。如果一个程序使用一个指令集而另一程序使用另一个指令集,则会导致仿真不一致。
x87指令集包括一个“控制字”寄存器,其中包含一些标志,这些标志控制浮点运算的某些方面(例如,精确的舍入行为等)。这是运行时的事情,您的程序可以执行一组计算,然后更改该寄存器,然后执行完全相同的计算并获得不同的结果。显然,必须检查和处理该寄存器,并在不同的机器上保持相同。编译器(或您在程序中使用的库)可能会生成代码,这些代码会在运行时在整个程序中不一致地更改这些标志。
同样,在使用x87指令集的情况下,英特尔和AMD历来在实现方面有所不同。例如,一个供应商的CPU可能在内部使用比另一个供应商更多的位进行一些计算(因此得出的结果更准确),这意味着如果您碰巧在两个不同供应商的两个不同CPU(均为x86)上运行,则简单计算的结果 可能 会不同。我不知道如何以及在什么情况下可以启用这些更高的精度计算,以及它们是否在正常运行条件下发生,还是您必须专门询问,但我确实知道这些差异存在。
随机数以及在程序之间一致且确定地生成它们与浮点一致性无关。它很重要,并且是许多错误的源头,但最后,您只需要保持更多状态就可以同步。
以下是一些可能会有所帮助的技术:
一些项目使用“ 定点 ”数字和定点算法来避免舍入误差和浮点数的一般不可预测性。阅读Wikipedia文章以获取更多信息和外部链接。
在我自己的一个项目中,在开发过程中,我曾经对游戏所有实例中的所有相关状态(包括很多浮点数)进行哈希处理,并在每一帧通过网络发送哈希值,以确保均匀这种状态在不同的机器上没有什么不同。这也有助于进行调试,而不是让我不敢看我的眼睛去查看何时何地存在不一致(无论如何也不会告诉我它们起源于何处),我会立即知道在一台机器上游戏状态的某些部分开始发散从其他对象,并确切地知道它是什么(如果哈希检查失败,我将停止模拟并开始比较整个状态。) 此功能从一开始就在该代码库中实现,并且仅在开发过程中用于帮助调试(因为它具有性能和内存成本)。
更新 (回答下面的第一条评论):正如我在第1点中所说的,其他人在其他答案中所说的那样,这并不能保证任何事情。如果这样做, 可能会 降低发生不一致的可能性和频率,但是可能性不会变为零。如果您不仔细,系统地分析代码中正在发生的事情以及可能的问题根源,那么无论您“舍入”多少数字,仍然有可能遇到错误。
例如,如果您有两个数字(例如,应该产生相同结果的两个计算结果),分别为1.111499999和1.111500001,并将它们四舍五入到小数点后三位,则它们分别变为1.111和1.112。原始数字的差异仅为2E-9,但现在变成1E-3。实际上,您已将错误增加了500‘000次。即使四舍五入,它们仍然不相等。您加剧了这个问题。
没错,这种情况发生得并不多,我给出的示例是在这种情况下遇到的两个不幸数字,但是仍然可以用这些数字找到自己。而当您这样做时,您就会遇到麻烦。即使您使用定点算术或其他方法,唯一可行的解决方案是对所有可能出现的问题区域进行严格而系统的数学分析,并证明它们在整个程序中将保持一致。
简而言之,对于我们这些凡人,您需要有一种不透水的方式来监控情况,并准确地发现何时以及如何出现最小的差异,以便事后解决问题(而不是依靠您的眼睛)以查看游戏动画或对象移动或身体行为方面的问题。)