我想知道使用浮点运算的 C 或 C++ 中的任何代码是否会在任何基于 x86 的体系结构中产生位精确的结果,而不管代码的复杂性如何。
据我所知,自 Intel 8087 以来的任何 x86 架构都使用准备好处理 IEEE-754 浮点数的 FPU 单元,我看不出结果在不同架构中会有所不同的任何原因。但是,如果它们不同(即由于不同的编译器或不同的优化级别),是否有某种方法可以通过仅配置编译器来产生位精确的结果?
目录:
不,完全符合 ISO C11 和 IEEE 的 C 实现不保证与其他 C 实现的位相同,即使是同一硬件上的其他实现也是如此。
(首先,我将假设我们正在讨论正常的 C 实现,其中double
IEEE -754 binary64 格式等,即使 x86 上的 C 实现使用其他格式是合法的double
并使用软件仿真实现 FP 数学,并在 中定义限制float.h
。当并非所有 x86 CPU 都包含在 FPU 中时,这可能是合理的,但在 2016 年那是Deathstation 9000领域。)
相关:Bruce Dawson 的Floating-Point Determinism博客文章是对这个问题的回答。他的开场白很有趣(然后是很多有趣的东西):
IEEE 浮点数学是确定性的吗?你总是会从相同的输入中得到相同的结果吗?答案是明确的“是”。不幸的是,答案也是明确的“不”。恐怕你需要澄清你的问题。
如果您正在思考这个问题,那么您肯定会想看看Bruce关于浮点数学的系列文章的索引,这些文章由 x86 上的 C 编译器以及 asm 和一般的 IEEE FP 实现。
第一个问题:只需要“基本操作”:+ - * / 和 sqrt 才能返回“正确舍入”的结果,即 <= 0.5ulp 的错误,正确舍入到尾数的最后一位,所以结果是最接近精确结果的可表示值。
其他数学库函数,如pow()
、log()
和 ,sin()
允许实现者在速度和准确性之间进行权衡。例如,glibc 通常有利于准确性,并且对于某些函数 IIRC,它比 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 = 2
80 位精度的行为。(该标准规定,每次赋值仍会进行舍入,但像 gcc 这样的真正编译器实际上并不会为舍入执行额外的存储/重新加载,除非您使用-ffloat-store
. 请参阅https://gcc.gnu.org/wiki/FloatingPointMath。该标准似乎是在假设非优化编译器或硬件有效地提供对类型宽度的舍入的情况下编写的,例如非 x86,或者像 x87 一样,精度设置为舍入到 64 位double
而不是 80 位long double
。在每个语句之后存储正是gcc -O0
和大多数其他编译器一样,并且该标准允许在评估一个表达式时获得额外的精度。)
因此,当以 x87 为目标时,允许编译器float
使用两条 x87FADD
指令计算三个 s 的总和,而不会将前两个指令的总和四舍五入为 32-bit float
。在那种情况下,临时具有 80 位精度......或者是吗?并非总是如此,因为 C 实现的启动代码(或 Direct3D 库!!!)可能已更改 x87 控制字中的精度设置,因此 x87 寄存器中的值四舍五入为 53 或 24 位尾数。(这使得 FDIV 和 FSQRT 运行得更快一些。)所有这些都来自Bruce Dawson 关于中间 FP 精度的文章)。
在舍入模式和精度设置相同的情况下,我认为每个 x86 CPU 都应该为相同的输入提供相同的结果,即使对于像 FSIN 这样的复杂 x87 指令也是如此。
英特尔的手册并未准确定义每种情况下的结果,但我认为英特尔的目标是实现位精确的向后兼容性。例如,我怀疑他们是否会为 FSIN 添加扩展精度范围缩减。它使用您获得的 80 位 pi 常数fldpi
(正确舍入的 64 位尾数,实际上是 66 位,因为精确值的下 2 位为零)。英特尔关于最坏情况错误的文档减少了 1.3 万亿分之一,直到他们在布鲁斯道森注意到最坏情况实际上有多糟糕后对其进行了更新。但这只能通过扩展精度范围缩小来解决,因此在硬件上并不便宜。
我不知道 AMD 是否实施了他们的 FSIN 和其他微编码指令以始终向英特尔提供位相同的结果,但我不会感到惊讶。我认为有些软件确实依赖它。
由于SSE 仅提供 add/sub/mul/div/sqrt的说明,因此没什么好说的。它们完全实现了 IEEE 操作,因此任何 x86 实现都不会给您带来任何不同(除非舍入模式设置不同,或者非正规为零和/或刷新为零不同,并且您有任何非正规)。
SSE rsqrt
(快速近似倒数平方根)没有完全指定,我认为即使在牛顿迭代之后你可能会得到不同的结果,但除了 SSE/SSE2 在 asm 中总是有点精确,假设 MXCSR 不是设置怪异。所以唯一的问题是让编译器生成相同的代码,或者只使用相同的二进制文件。
因此,如果您静态链接libm
使用 SSE/SSE2 并分发这些二进制文件,它们将在任何地方运行相同。除非该库使用运行时 CPU 检测来选择替代实现......
正如@Yan Zhou 指出的那样,您几乎需要将实现的每一点都控制到 asm 以获得精确的结果。
但是,有些游戏确实依赖于多人游戏,但通常会检测/纠正不同步的客户端。不是每帧都通过网络发送整个游戏状态,而是每个客户端计算接下来会发生什么。如果游戏引擎被仔细地实现为确定性的,它们就会保持同步。
在 Spring RTS 中,客户端校验其游戏状态以检测 desync。我有一段时间没玩了,但我记得至少在 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 网站上购买标准论文。不幸的是,这不是公开的