当然,跨平台和交叉编译器的一致性是可能的。只要有足够的知识和时间,任何事情都是可能的!但这可能非常困难、耗时或者不切实际。
以下是我可以预见到的问题(无特定顺序):
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倍。即使进行了舍入,它们仍然不相等。这只会加剧问题。
诚然,这种情况并不经常发生,我给出的示例是两个不幸的数字,但仍有可能遇到这些数字。当你遇到这种情况时,就会遇到麻烦。唯一确保没有问题的解决方案,即使你使用固定点算法或其他算法,也是对所有可能出现问题的区域进行严格而系统的数学分析,并证明它们将在程序中保持一致。
除此之外,在我们这些凡人看来,你需要有一种完全可靠的方法来监控情况,并找出最微小的差异何时以及如何发生,以便能够事后解决问题(而不是依赖于肉眼来观察游戏动画或对象移动或物理行为中的问题)。