.NET Core中gamedev的浮点确定性

问题描述 投票:1回答:2

背景

我们正在使用C#和.NET Core开发RTS game engine。与大多数其他实时多人游戏不同,RTS游戏倾向于通过将玩家输入与其他玩家同步,并同时在所有客户端上同步运行游戏模拟。这要求游戏逻辑具有确定性,以便游戏不会失去同步。

非确定性的一个潜在来源是浮点运算。从我收集到的主要问题是旧的x87 FPU指令 - 它们使用内部80位寄存器,而IEEE-754浮点值是32位或64位,因此当从寄存器移动时值被截断记忆。对代码和/或编译器的微小更改可能导致在不同时间发生截断,从而导致稍微不同的结果。非确定性也可能是由意外使用不同的FP舍入模式引起的,但如果我理解正确,这主要是一个已解决的问题。

我也gotten the impression SSE(2)指令不受截断问题的影响,因为它们在没有更高精度寄存器的情况下执行32位或64位的所有浮点运算。

最后,据我所知,CLR在x86上使用x87 FPU指令(或至少在RyuJIT之前使用的情况),以及x86-64上的SSE指令。我不确定这是否适用于所有或大多数操作。

如果重要的话,最近已经在.NET Core中添加了对准确的single precision math的支持。

但是,在研究是否可以在.NET中确定性地使用浮点时,有许多答案说不,尽管它们主要涉及运行时的旧版本。

  • 在2013年的StackOverflow answer中,Eric Lippert说如果你想保证在.NET中可重复的算术,你应该“使用整数”。
  • 在一个关于Roslyn的GitHub页面上的主题的讨论中,游戏开发者said in a comment在2017年表示他们无法在C#中实现可重复的浮点运算,尽管他没有指定他们使用的运行时。
  • 在2011年游戏开发堆栈交换answer中,作者得出结论,他无法在.NET中获得可靠的FP算法。他为.NET提供了一个基于软件的浮点实现,它与IEEE754浮点二进制兼容。

这个问题

因此,如果CoreCLR在x86-64上使用SSE FP指令,这是否意味着它不会遭受截断问题和/或任何其他与FP相关的非确定性问题?我们使用引擎发布.NET Core,因此每个客户端都使用相同的运行时,我们要求玩家使用完全相同版本的游戏客户端。将引擎限制为仅在x86-64(在PC上)上工作也是可接受的限制。

如果运行时仍然使用具有不可靠结果的x87指令,那么使用软件浮点实现(如上面的答案中链接的那个)来计算单个值的计算是否有意义,并使用新的hardware intrinsics加速SSE的向量运算?我已经制作了这个原型并且似乎是有效的,但这是不必要的吗?

如果我们可以只使用正常的浮点运算,我们应该避免什么,比如三角函数?

最后,如果到目前为止一切正常,当不同的客户端使用不同的操作系统甚至不同的CPU架构时,这将如何工作?现代ARM CPU是否会受到80位截断问题的影响,或者相同的代码是否与x86完全相同(如果我们排除三角函数等棘手的东西),假设实现没有错误?

c# .net-core floating-point sse ieee-754
2个回答
1
投票

因此,如果CoreCLR在x86-64上使用SSE FP指令,这是否意味着它不会遭受截断问题和/或任何其他与FP相关的非确定性问题?

如果您继续使用x86-64并且在任何地方使用完全相同版本的CoreCLR,那么它应该是确定性的。

如果运行时仍然使用具有不可靠结果的x87指令,那么使用软件浮动实现是否有意义[...]我已经对其进行了原型设计并且似乎是有效的,但这是不必要的吗?

它可能是一个解决JIT问题的解决方案,但您可能必须开发一个Roslyn分析器,以确保您不使用浮点运算而不通过这些...或编写一个IL重写器来执行此操作你(但这会使你的.NET程序集依赖于...根据你的要求可以接受)

如果我们可以只使用正常的浮点运算,我们应该避免什么,比如三角函数?

据我所知,CoreCLR正在将数学函数重定向到编译器libc,所以只要你保持相同的版本,相同的平台,它应该没问题。

最后,如果到目前为止一切正常,当不同的客户端使用不同的操作系统甚至不同的CPU架构时,这将如何工作?现代ARM CPU是否会受到80位截断问题的影响,或者相同的代码是否与x86完全相同(如果我们排除三角函数等棘手的东西),假设实现没有错误?

您可以遇到一些与额外精度无关的问题。例如,对于ARMv7,子异常浮点数被刷新为零,而aarch64上的ARMv8将保留它们。

所以假设你留在ARMv8上,我不清楚ARMv8的JIT CoreCLR是否在这方面表现得很好;你应该直接在GitHub上询问。 libc的行为仍然可能会破坏确定性结果。

我们正在使用我们的“突发”编译器在Unity上解决这个问题,将.NET IL转换为本机代码。我们在所有机器上使用LLVM codegen,禁用一些可能破坏确定性的优化(所以在这里,我们总体上可以尝试保证编译器在各个平台上的行为),并且我们也使用SLEEF库来提供确定性计算数学函数(例如参见https://github.com/shibatch/sleef/issues/187)......所以可以这样做。

在你的位置,我可能会尝试调查CoreCLR是否真的可以确定x64和ARMv8之间的普通浮点运算......如果它看起来没问题,你可以调用这些SLEEF函数而不是System.Math,它可以开箱即用,或者建议CoreCLR从libc切换到SLEEF。


0
投票

更像是思考的食物,而不是一个明确的答案:你可能想要研究除.NET内置的数字类型之外的数字类型。显而易见的缺点是,.NET中的内容不仅很好理解(嗯),而且硬件支持在每个平台上都有很多。但是,仍然可以查看posits,一个新的,仍在进行中的浮点数格式。

正面标准不会以导致问题的方式留出解释空间,并且内置累加器也是内置的。因此,posit操作在平台上产生确定性结果 - 理论上,因为硬件实现是稀疏的(但存在!),并且没有现成的CPU本地支持它。因此,您可能只将它用作软数字类型,但如果此类计算位于延迟敏感的执行路径上,则这可能只是一个问题。

还有一个.NET库,你可以找到here(目标.NET Framework,但很容易切换到.NET标准),也可以转换为FPGA硬件实现。更多信息是here

免责声明:我来自.NET库背后的公司(但是我们并没有发明这个假设)。

© www.soinside.com 2019 - 2024. All rights reserved.