在任何基于x86的体系结构中,是否有浮点密集型代码会产生位精确的结果?

Sam*_*Lou 5 c x86 ieee-754 fast-math

我想知道在C或C ++中使用浮点运算的任何代码在任何基于x86的体系结构中是否都会产生精确的结果,而不管代码的复杂性如何。

据我所知,自从Intel 8087开始,任何x86架构都使用了准备处理IEEE-754浮点数的FPU单元,而且我看不出任何原因导致不同架构的结果不同。但是,如果它们不同(即由于不同的编译器或不同的优化级别),那么是否有某种方法可以通过仅配置编译器来产生位精确结果?

Pet*_*des 9

目录:

  • C / C ++
  • 汇编
  • 创建实现这一目标的现实软件。

在C或C ++中:

不可以,完全符合ISO C11和IEEE的C实现不能保证与其他C实现(甚至同一硬件上的其他实现)完全相同的结果。

(和首先,我要假设我们正在谈论普通的C实现中doubleIEEE-754 binary64格式等,即使它在x86 C实现使用一些其他的格式是合法的double并通过软件仿真实现FP数学,并在中定义限制float.h。当并非所有FPU都包含x86 CPU,但在2016年为Deathstation 9000时,这可能是合理的。)


相关文章:Bruce Dawson的Floating-Point Determinism博客文章是此问题的答案。他的开场白很有趣(紧随其后的是很多有趣的东西):

IEEE浮点数学是否具有确定性?您将始终从相同的输入获得相同的结果吗?答案是明确的“是”。不幸的是,答案也是明确的“否”。恐怕您需要澄清您的问题。

如果您正在考虑这个问题,那么您肯定会想看看Bruce关于浮点数学的系列文章的索引该索引由x86的C编译器以及asm和一般的IEEE FP实现。


第一个问题:仅需要“基本运算”:+-* /和sqrt才能返回“正确舍入”的结果,即<= 0.5ulp错误,正确舍入到尾数的最后一位,因此结果是最接近可表示值的确切结果。

其他数学库功能(如pow()log()和)sin()允许实现者在速度和准确性之间进行权衡。例如,对于某些功能IIRC,glibc通常有利于准确性,并且比Apple的OS X数学库慢。另请参见glibc有关跨不同体系结构的每个libm函数的错误范围的文档


但是,等等,情况变得更糟。即使仅使用正确四舍五入的基本操作的代码也不能保证相同的结果。

C规则还允许在保留更高精度的临时对象方面具有一定的灵活性。该实现定义为FLT_EVAL_METHOD使代码可以检测其工作方式,但是如果您不喜欢该实现的工作,则无法选择。您确实可以选择(使用#pragma STDC FP_CONTRACT off)来禁止编译器a*b + c进入FMA,例如a*b在添加之前不进行临时取整。

在x86上,面向32位非SSE代码的编译器(即,使用过时的x87指令)通常在两次操作之间将FP临时变量保留在x87寄存器中。这将产生FLT_EVAL_METHOD = 280位精度的行为。(标准规定,四舍五入仍会在每次分配上进行,但是真正的编译器(如gcc)除非使用,否则实际上不会进行额外的存储/重载以进行四舍五入-ffloat-store。请参阅https://gcc.gnu.org/wiki/FloatingPointMath。该标准似乎是在假设非优化编译器的情况下编写的,或者是像非x86或x87这样有效地提供舍入到类型宽度的硬件,或者将x87的精度设置为舍入为64位double而不是80位的精度long double到底是什么gcc -O0以及其他大多数编译器都可以使用,并且该标准允许对一个表达式进行求值时具有更高的精度。)

因此,在针对x87时,允许编译器float使用两条x87 FADD指令来评估3的总和,而无需将前两个之和四舍五入为32位float。在这种情况下,临时文件的精度为80位...还是?并非总是如此,因为C实现的启动代码(或Direct3D库!!!)可能已经更改了x87控制字中的精度设置,因此x87寄存器中的值四舍五入为53或24位尾数。(这使FDIV和FSQRT的运行速度更快。)所有这些均来自Bruce Dawson关于中等FP精度的文章


组装中:

在舍入模式和精度设置相同的情况下,我认为每个x86 CPU都应为相同的输入给出位相同的结果,即使对于复杂的x87指令(如FSIN)也是如此。

英特尔的手册并未确切定义每种情况下的结果,但我认为英特尔的目标是实现位精确的向后兼容性。我怀疑他们是否会为FSIN添加扩展精度范围缩小。它使用您获得的80位pi常数fldpi(正确舍入的64位尾数,实际上是66位,因为精确值的后2位为零)。在布鲁斯·道森(Bruce Dawson)注意到最糟糕的情况实际上很糟糕之后,英特尔对其进行了更新,直到最迟错误的文档减少了1.3倍。但这只能通过扩大精度范围来解决,因此在硬件上并不便宜。

我不知道AMD是否实现了他们的FSIN和其他微编码指令来始终为英特尔提供与位相同的结果,但是我不会感到惊讶。我认为某些软件确实依赖它。


由于SSE仅提供有关add / sub / mul / div / sqrt的说明,因此没有什么好说的了。它们完全实现IEEE操作,因此任何x86实现都不会给您带来任何不同的结果(除非舍入模式设置不同,或者非正态为零和/或刷新为零是不同的,并且您拥有异常)。

SSE rsqrt(快速近似倒数平方根)没有确切定义的,而且我认为这是可能的,即使牛顿迭代后,你可能会得到不同的结果,但比SSE / SSE2总是在ASM位准确等,假设MXCSR不设置怪异的。因此,唯一的问题是让编译器生成相同的代码,或仅使用相同的二进制文件。


在真实生活中:

因此,如果您静态链接libm使用SSE / SSE2的a并分发这些二进制文件,则它们将在所有地方运行相同的文件。除非该库使用运行时CPU检测来选择替代实现...

正如@Yan Zhou指出的那样,您几乎需要控制实现的每一部分,直到asm才能获得精确的结果。

但是,对于多人游戏,某些游戏确实确实依赖于此功能,但对于不同步的客户端通常需要进行检测/纠正。每个客户端无需计算每帧通过网络发送的整个游戏状态,而是可以计算接下来发生的事情。如果游戏引擎经过精心设计以具有确定性,它们将保持同步。

在Spring RTS中,客户端对自己的游戏状态进行校验和以检测desync。我已经有一段时间没玩了,但我确实记得至少在5年前读过一些有关他们如何通过确保所有x86版本都使用SSE数学甚至32位版本来实现同步的知识。

某些游戏不允许在PC和非x86控制台系统之间进行多人游戏的一个可能原因是,该引擎在所有PC上给出的结果相同,但在使用不同编译器的不同体系结构的控制台上给出的结果却不同。

进一步阅读:GAFFER ON GAMES:浮点确定性。实际游戏引擎使用某些技术来获得确定性的结果。例如,将sin / cos / tan包装在未优化的函数调用中,以强制编译器将其保留为单精度。