.NET Core中Gamedev的浮点确定性

时间:2018-12-31 16:32:21

标签: c# .net-core floating-point sse ieee-754

背景

我们正在使用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指令产生不可靠的结果,那么使用软件浮点实现(如上面的答案中链接的那个)进行有关单个值的计算是否有意义,并使用新的{ {3}}?我已经对此进行了原型设计,这似乎可行,但这是否不必要?

如果我们只能使用普通的浮点运算,是否应该避免使用三角函数?

最后,如果到目前为止一切正常,那么当不同的客户端使用不同的操作系统甚至不同的CPU体系结构时,这将如何工作?假设实现没有错误,现代的ARM CPU是否会遭受80位截断问题的困扰,或者相同的代码是否可以与x86相同地运行(如果我们排除三角函数之类的棘手东西)?

2 个答案:

答案 0 :(得分:1)

  

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

如果您使用的是x86-64,并且在各处都使用完全相同的CoreCLR版本,那么它应该是确定性的。

  

如果运行时仍使用x87指令产生不可靠的结果,那么使用软件浮点实现是有意义的吗?我已经对此进行了原型设计,并且似乎可以使用,但是没有必要吗?

这可能是解决JIT问题的一种解决方案,但是您可能必须开发一个Roslyn分析器,以确保在不进行这些操作的情况下不使用浮点运算...或者编写将为您执行此操作(但是这会使您的.NET程序集成为拱形依赖项...根据您的要求,可以接受)

  

如果我们只能使用普通的浮点运算,是否应该避免使用三角函数?

据我所知,CoreCLR正在将数学函数重定向到编译器libc,因此,只要您使用的是同一版本,同一平台,就可以了。

  

最后,如果到目前为止一切正常,那么当不同的客户端使用不同的操作系统甚至不同的CPU体系结构时,这将如何工作?假设实现没有错误,现代的ARM CPU是否会遭受80位截断问题的困扰,或者相同的代码是否可以与x86相同地运行(如果我们排除三角函数之类的棘手东西)?

您可能会遇到一些与超高精度无关的问题。例如,对于ARMv7,次标准浮点数将刷新为零,而aarch64上的ARMv8将保留它们。

因此,假设您使用的是ARMv8,那么我不太清楚ARMv8的JIT CoreCLR在这方面的表现如何;您可能应该直接在GitHub上询问。 libc的行为仍然可能破坏确定性结果。

我们正致力于在Unity上的“突发”编译器上解决此问题,以将.NET IL转换为本机代码。我们在所有机器上使用LLVM代码生成器,禁用了一些可能破坏确定性的优化(因此,在这里,总体而言,我们可以尝试确保跨平台的编译器的行为),并且我们还使用SLEEF库来提供确定性计算数学函数(例如,参见https://github.com/shibatch/sleef/issues/187)…因此可以实现。

在您的位置上,我可能会尝试研究CoreCLR是否真的确定性地适用于x64和ARMv8之间的纯浮点运算……如果看起来还可以,您可以调用这些SLEEF函数而不是System.Math,并且开箱即用,或建议CoreCLR从libc切换到SLEEF。

答案 1 :(得分:0)

更像是一种深思熟虑的问题,而不是一个明确的答案:您可能想研究.NET中内置的数字类型以外的其他数字类型。明显的缺点是,.NET中的内容不仅得到了很好的理解(hmm),而且每个平台上也都存在很多硬件支持。但是,仍然可以签出posits(一种仍在进行中的新的浮点数格式)。

posit标准不会以引起问题的方式进行解释,也内置了一个内部累加器。因此,假定操作在整个平台上产生确定性的结果-从理论上讲,因为硬件实现稀疏(但存在!),并且没有现成的CPU本地支持它。因此,您只能将其用作软数字类型,尽管如果这种计算是在对延迟敏感的执行路径上进行的,这可能只是个问题。

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

免责声明:我来自.NET库背后的公司(但是posit不是我们发明的)。