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

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

据我所知,自英特尔8087以来的任何x86架构都使用准备处理IEEE-754浮点数的FPU单元,我看不出为什么结果在不同架构中会有所不同的任何原因。 但是,如果它们不同(即由于不同的编译器或不同的优化级别),是否可以通过配置编译器来产生位精确结果?

目录:

  • C / C ++
  • ASM
  • 创建实现此目的的真实软件。

在C或C ++中:

不,完全符合ISO C11和IEEE标准的C实现不保证与其他C实现的位相同的结果,甚至是同一硬件上的其他实现。

(首先,我将假设我们正在谈论正常的C实现,其中double是IEEE-754二进制64格式等,即使在x86上的C实现使用其他格式也是合法的对于double和使用软件仿真实现FP数学,并在float.h定义限制。当并非所有x86 CPU都包含在FPU中时,这可能是合理的,但在2016年是Deathstation 9000领域。)


相关:布鲁斯道森的浮点决定论博客文章是这个问题的答案。 他的开头段很有趣(后面跟着很多有趣的东西):

IEEE浮点数学是否具有确定性? 您是否总能从相同的输入获得相同的结果? 答案是明确的“是”。 不幸的是,答案也是明确的“不”。 我担心你需要澄清你的问题。

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


第一个问题 :只有“基本操作”:+ – * /和sqrt需要返回“正确舍入”的结果 ,即<= 0.5ulp的错误,正确舍入到尾数的最后一位,所以结果是与确切结果最接近的可表示值。

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


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

C规则还允许在保持更高精度的临时性方面具有一定的灵活性。 该实现定义了FLT_EVAL_METHOD因此代码可以检测它是如何工作的,但如果您不喜欢实现的function,则无法做出选择。 您可以选择( #pragma STDC FP_CONTRACT off )禁止编译器例如将a*b + c转换为FMA,而在添加之前没有a*b临时舍入。

在x86上,针对32位非SSE代码的编译器(即使用过时的x87指令 )通常会在操作之间将FP临时值保留在x87寄存器中。 这会产生80位精度的FLT_EVAL_METHOD = 2行为。 (该标准指定舍入仍然发生在每个任务上,但是像gcc这样的实际编译器实际上并没有进行额外的存储/重新加载以进行舍入,除非你使用-ffloat-store 。请参阅https://gcc.gnu.org/wiki/FloatingPointMath标准的那部分似乎是在假设非优化编译器,或者有效地提供类型宽度舍入的硬件(如非x86),或类似x87,精度设置为舍入到64位double而不是80位在每个语句之后存储正是gcc -O0和大多数其他编译器所做的 ,标准允许在评估一个表达式时提供额外的精度。)

因此,在针对x87时,允许编译器使用两个x87 FADD指令计算三个float的总和,而不会将前两个的总和与32位float相加。 在那种情况下,临时具有80位精度……或者它呢? 并非总是如此,因为C实现的启动代码(或Direct3D库!!!)可能已经改变了x87控制字中的精度设置,因此x87寄存器中的舍入为53或24位尾数。 (这使得FDIV和FSQRT运行得更快。)所有这些都来自Bruce Dawson关于中间FP精度的文章 。


在组装中:

在舍入模式和精度设置相同的情况下,我认为每个x86 CPU应该为相同的输入提供相同的结果,即使对于像FSIN这样的复杂x87指令也是如此。

英特尔的手册并未准确定义每种情况下的结果,但我认为英特尔旨在实现精确的向后兼容性。 我怀疑他们是否会为FSIN增加扩展精度范围缩减,例如。 它使用fldpi得到的80位pi常量(正确舍入的64位尾数,实际上是66位,因为精确值的下2位为零)。 在布鲁斯道森注意到最坏情况实际上有多糟糕之后,英特尔关于最坏情况错误的文档被关闭了1.3个百分点, 直到他们更新了它 。 但这只能通过扩展精度范围缩减来解决,因此在硬件上并不便宜。

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


由于SSE仅提供add / sub / mul / div / sqrt的指令 ,因此没有什么可说的。 它们完全实现了IEEE操作,因此任何x86实现都不会给你任何不同的东西(除非舍入模式设置不同,或者denormals-are-zero和/或flush-to-zero是不同的,你有任何非正规数)。

SSE rsqrt (快速近似倒数平方根) 没有精确指定 ,我认为即使在牛顿迭代之后你也可能得到不同的结果,但除了SSE / SSE2在asm中总是有点精确 ,假设MXCSR不是’设置很奇怪。 所以唯一的问题是让编译器生成相同的代码,或者只使用相同的二进制文件。


在真实生活中:

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

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

但是,有些游戏确实依赖于多玩家的游戏,但通常会对不同步的客户进行检测/纠正 。 每个客户端不是每帧都通过网络发送整个游戏状态,而是计算接下来发生的事情。 如果游戏引擎经过精心实施以确定性,则它们保持同步。

在Spring RTS中, 客户端校验其游戏状态以检测异步 。 我有一段时间没有播放它,但我记得至少在5年前读过一些关于他们尝试通过确保所有x86版本都使用SSE数学,甚至是32位版本来实现同步的东西。

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

进一步阅读: GAFFER ON GAMES:浮点决定论 。 真实游戏引擎用于获得确定性结果的一些技术。 例如,在非优化函数调用中包装sin / cos / tan以强制编译器以单精度保留它们。

如果编译器和体系结构符合IEEE标准,那么。

例如,如果配置正确,gcc符合IEEE标准。 如果使用-ffast-math标志,则不符合IEEE标准。

请参见http://www.validlab.com/goldberg/paper.pdf第25页。

如果您想确切知道在使用IEEE 754-1985硬件/编译器对时可以依赖的准确性,则需要在IEEE网站上购买标准纸。 不幸的是,这不是公开的

http://ieeexplore.ieee.org/xpl/login.jsp?tp=&arnumber=30711&url=http%3A%2F%2Fieeexplore.ieee.org%2Fstamp%2Fstamp.jsp%3Ftp%3D%26arnumber%3D30711