.NET Core 中 gamedev 的浮点确定性
Floating point determinism for gamedev in .NET Core
背景
我们正在使用 C# 和 .NET Core 开发 RTS game engine。与大多数其他实时多人游戏不同,RTS 游戏倾向于通过将玩家输入同步给其他玩家来工作,并且 运行 同时在所有客户端上同步进行游戏模拟。这要求游戏逻辑是确定性的,这样游戏就不会失去同步。
不确定性的一个潜在来源是浮点运算。据我所知,主要问题是旧的 x87 FPU 指令 - 它们使用内部 80 位寄存器,而 IEEE-754 浮点值是 32 位或 64 位,因此值为 t运行cated 当从寄存器移动到内存时。对代码 and/or 编译器的小改动可能会导致 t运行cation 在不同时间发生,从而导致结果略有不同。不确定性也可能是由于意外使用不同的 FP 舍入模式引起的,但如果我理解正确的话,这基本上是一个已解决的问题。
我还 gotten the impression SSE(2) 指令不受 t运行cation 问题的影响,因为它们在 32 位或 64 位中执行所有浮点运算而无需更高精度的寄存器。
最后,据我所知,CLR 在 x86 上使用 x87 FPU 指令(或者至少在 RyuJIT 之前是这样),在 x86-64 上使用 SSE 指令。我不确定这是否意味着所有或大多数操作。
如果重要的话,最近已将对准确 single precision math 的支持添加到 .NET Core。
但是在研究是否可以在 .NET 中确定性地使用浮点数时,有很多答案都说不,尽管它们主要涉及 运行time 的旧版本。
- 在 2013 年的 Whosebug answer 中,Eric Lippert 说如果你想保证 .NET 中的可重现算法,你应该 "Use integers".
- 2017 年,在 Roslyn 的 GitHub 页面上讨论了一个游戏开发者 said in a comment 的主题,他们无法在 C# 中实现可重复的浮点运算,尽管他没有具体说明是哪个 运行他们使用的时间。
- 在 2011 年游戏开发堆栈交换 answer 中,作者得出结论,他无法在 .NET 中获得可靠的 FP 算法。他为 .NET 提供了基于软件的浮点实现,与 IEEE754 浮点二进制兼容。
问题
因此,如果 CoreCLR 在 x86-64 上使用 SSE FP 指令,是否意味着它不会遇到 t运行cation 问题,and/or 任何其他与 FP 相关的非-决定论?我们将 .NET Core 与引擎一起发布,因此每个客户端都将使用相同的 运行 时间,并且我们会要求玩家使用完全相同版本的游戏客户端。限制引擎仅在 x86-64(在 PC 上)上工作也是一个可以接受的限制。
如果 运行time 仍然使用结果不可靠的 x87 指令,使用软件浮点实现(如上面答案中链接的那个)来计算单个值并加速向量是否有意义使用新的 hardware intrinsics?我已经对此进行了原型设计,它似乎可行,但没有必要吗?
如果我们只能使用普通的浮点运算,有什么我们应该避免的,比如三角函数?
最后,如果到目前为止一切正常,当不同的客户端使用不同的操作系统甚至不同的 CPU 架构时,这将如何工作?现代 ARM CPU 是否会遇到 80 位 t运行 问题,或者相同的代码 运行 是否与 x86 相同(如果我们排除更棘手的东西,如三角函数),假设实现没有错误?
So, if CoreCLR uses SSE FP instructions on x86-64, does that mean that it doesn't suffer from the truncation issues, and/or any other FP-related non-determinism?
如果您继续使用 x86-64 并且在所有地方都使用完全相同的 CoreCLR 版本,那么它应该是确定性的。
If the runtime still uses x87 instructions with unreliable results, would it make sense to use a software float implementation [...] I've prototyped this and it seems to be work, but is it unnecessary?
这可能是解决 JIT 问题的解决方案,但您可能必须开发 Roslyn 分析器以确保您不使用浮点运算而不通过这些...或编写 IL 重写器这将为您执行此操作(但这会使您的 .NET 程序集依赖于架构......根据您的要求,这可能是可以接受的)
If we can just use normal floating point operations, is there anything we should avoid, like trigonometric functions?
据我所知,CoreCLR 正在将数学函数重定向到编译器 libc,所以只要您保持在同一版本、同一平台上,就应该没问题。
Finally, if everything is OK so far how would this work when different clients use different operating systems or even different CPU architectures? Do modern ARM CPUs suffer from the 80-bit truncation issue, or would the same code run identically to x86 (if we exclude trickier stuff like trigonometry), assuming the implementation has no bugs?
您可能会遇到一些与额外精度无关的问题。例如,对于 ARMv7,次正规浮点数被刷新为零,而 aarch64 上的 ARMv8 将保留它们。
假设您继续使用 ARMv8,我不太清楚 ARMv8 的 JIT CoreCLR 在这方面是否表现良好;您可能应该直接在 GitHub 上询问。还有 libc 的行为可能会破坏确定性结果。
我们正致力于在 Unity 中解决此问题,使用我们的 "burst" 编译器将 .NET IL 转换为本机代码。我们在所有机器上使用 LLVM codegen,禁用一些可能破坏确定性的优化(所以在这里,总体上我们可以尝试保证跨平台编译器的行为),我们还使用 SLEEF 库来提供确定性计算数学函数(参见 https://github.com/shibatch/sleef/issues/187 的例子)……所以可以做到。
在你的位置上,我可能会尝试调查 CoreCLR 是否真的对 x64 和 ARMv8 之间的普通浮点运算具有确定性......如果它看起来没问题,你可以调用这些 SLEEF 函数而不是 System.Math
和它可以开箱即用,或者建议 CoreCLR 从 libc 切换到 SLEEF。
与其说是明确的答案,不如说更像是深思熟虑:您可能想要研究 .NET 中内置的数字类型以外的数字类型。缺点显然是 .NET 中的内容不仅易于理解 (hmm),而且几乎每个平台都提供硬件支持。但是,仍然可以查看 posits,一种新的、仍在开发中的浮点数格式。
posit 标准没有为导致您的问题的方式留下解释空间,并且还内置了一个内部累加器。因此 posit 操作产生跨平台的确定性结果 - 理论上,因为硬件实现很少(但存在!),并且没有现成的 CPU 本机支持它。因此,您只能将其用作软数字类型,但如果此类计算位于对延迟敏感的执行路径上,这对您来说可能只是一个问题。
还有一个 .NET 库,您可以找到 here (targets .NET Framework but can very easily be switched over to .NET Standard) which can also be turned into an FPGA hardware implementation. More info is here。
免责声明:我来自 .NET 库背后的公司(但 posit 不是我们发明的)。
背景
我们正在使用 C# 和 .NET Core 开发 RTS game engine。与大多数其他实时多人游戏不同,RTS 游戏倾向于通过将玩家输入同步给其他玩家来工作,并且 运行 同时在所有客户端上同步进行游戏模拟。这要求游戏逻辑是确定性的,这样游戏就不会失去同步。
不确定性的一个潜在来源是浮点运算。据我所知,主要问题是旧的 x87 FPU 指令 - 它们使用内部 80 位寄存器,而 IEEE-754 浮点值是 32 位或 64 位,因此值为 t运行cated 当从寄存器移动到内存时。对代码 and/or 编译器的小改动可能会导致 t运行cation 在不同时间发生,从而导致结果略有不同。不确定性也可能是由于意外使用不同的 FP 舍入模式引起的,但如果我理解正确的话,这基本上是一个已解决的问题。
我还 gotten the impression SSE(2) 指令不受 t运行cation 问题的影响,因为它们在 32 位或 64 位中执行所有浮点运算而无需更高精度的寄存器。
最后,据我所知,CLR 在 x86 上使用 x87 FPU 指令(或者至少在 RyuJIT 之前是这样),在 x86-64 上使用 SSE 指令。我不确定这是否意味着所有或大多数操作。
如果重要的话,最近已将对准确 single precision math 的支持添加到 .NET Core。
但是在研究是否可以在 .NET 中确定性地使用浮点数时,有很多答案都说不,尽管它们主要涉及 运行time 的旧版本。
- 在 2013 年的 Whosebug answer 中,Eric Lippert 说如果你想保证 .NET 中的可重现算法,你应该 "Use integers".
- 2017 年,在 Roslyn 的 GitHub 页面上讨论了一个游戏开发者 said in a comment 的主题,他们无法在 C# 中实现可重复的浮点运算,尽管他没有具体说明是哪个 运行他们使用的时间。
- 在 2011 年游戏开发堆栈交换 answer 中,作者得出结论,他无法在 .NET 中获得可靠的 FP 算法。他为 .NET 提供了基于软件的浮点实现,与 IEEE754 浮点二进制兼容。
问题
因此,如果 CoreCLR 在 x86-64 上使用 SSE FP 指令,是否意味着它不会遇到 t运行cation 问题,and/or 任何其他与 FP 相关的非-决定论?我们将 .NET Core 与引擎一起发布,因此每个客户端都将使用相同的 运行 时间,并且我们会要求玩家使用完全相同版本的游戏客户端。限制引擎仅在 x86-64(在 PC 上)上工作也是一个可以接受的限制。
如果 运行time 仍然使用结果不可靠的 x87 指令,使用软件浮点实现(如上面答案中链接的那个)来计算单个值并加速向量是否有意义使用新的 hardware intrinsics?我已经对此进行了原型设计,它似乎可行,但没有必要吗?
如果我们只能使用普通的浮点运算,有什么我们应该避免的,比如三角函数?
最后,如果到目前为止一切正常,当不同的客户端使用不同的操作系统甚至不同的 CPU 架构时,这将如何工作?现代 ARM CPU 是否会遇到 80 位 t运行 问题,或者相同的代码 运行 是否与 x86 相同(如果我们排除更棘手的东西,如三角函数),假设实现没有错误?
So, if CoreCLR uses SSE FP instructions on x86-64, does that mean that it doesn't suffer from the truncation issues, and/or any other FP-related non-determinism?
如果您继续使用 x86-64 并且在所有地方都使用完全相同的 CoreCLR 版本,那么它应该是确定性的。
If the runtime still uses x87 instructions with unreliable results, would it make sense to use a software float implementation [...] I've prototyped this and it seems to be work, but is it unnecessary?
这可能是解决 JIT 问题的解决方案,但您可能必须开发 Roslyn 分析器以确保您不使用浮点运算而不通过这些...或编写 IL 重写器这将为您执行此操作(但这会使您的 .NET 程序集依赖于架构......根据您的要求,这可能是可以接受的)
If we can just use normal floating point operations, is there anything we should avoid, like trigonometric functions?
据我所知,CoreCLR 正在将数学函数重定向到编译器 libc,所以只要您保持在同一版本、同一平台上,就应该没问题。
Finally, if everything is OK so far how would this work when different clients use different operating systems or even different CPU architectures? Do modern ARM CPUs suffer from the 80-bit truncation issue, or would the same code run identically to x86 (if we exclude trickier stuff like trigonometry), assuming the implementation has no bugs?
您可能会遇到一些与额外精度无关的问题。例如,对于 ARMv7,次正规浮点数被刷新为零,而 aarch64 上的 ARMv8 将保留它们。
假设您继续使用 ARMv8,我不太清楚 ARMv8 的 JIT CoreCLR 在这方面是否表现良好;您可能应该直接在 GitHub 上询问。还有 libc 的行为可能会破坏确定性结果。
我们正致力于在 Unity 中解决此问题,使用我们的 "burst" 编译器将 .NET IL 转换为本机代码。我们在所有机器上使用 LLVM codegen,禁用一些可能破坏确定性的优化(所以在这里,总体上我们可以尝试保证跨平台编译器的行为),我们还使用 SLEEF 库来提供确定性计算数学函数(参见 https://github.com/shibatch/sleef/issues/187 的例子)……所以可以做到。
在你的位置上,我可能会尝试调查 CoreCLR 是否真的对 x64 和 ARMv8 之间的普通浮点运算具有确定性......如果它看起来没问题,你可以调用这些 SLEEF 函数而不是 System.Math
和它可以开箱即用,或者建议 CoreCLR 从 libc 切换到 SLEEF。
与其说是明确的答案,不如说更像是深思熟虑:您可能想要研究 .NET 中内置的数字类型以外的数字类型。缺点显然是 .NET 中的内容不仅易于理解 (hmm),而且几乎每个平台都提供硬件支持。但是,仍然可以查看 posits,一种新的、仍在开发中的浮点数格式。
posit 标准没有为导致您的问题的方式留下解释空间,并且还内置了一个内部累加器。因此 posit 操作产生跨平台的确定性结果 - 理论上,因为硬件实现很少(但存在!),并且没有现成的 CPU 本机支持它。因此,您只能将其用作软数字类型,但如果此类计算位于对延迟敏感的执行路径上,这对您来说可能只是一个问题。
还有一个 .NET 库,您可以找到 here (targets .NET Framework but can very easily be switched over to .NET Standard) which can also be turned into an FPGA hardware implementation. More info is here。
免责声明:我来自 .NET 库背后的公司(但 posit 不是我们发明的)。