据我了解,您有一个超越函数(如 sin(x))的软件实现,以 IEEE 标准操作(如浮点加法和乘法)表示,并且您希望确保在所有机器上得到相同的答案(或者,至少,你关心的所有机器)。
首先,了解:这不会适用于所有机器。例如IBM大型机十六进制浮点不是IEEE,并且不会给出相同的答案。为了得到准确的结果,您需要有一个 IEEEE 标准操作(如 FP 加法和乘法)的软件实现。
我猜你只关心实现 IEEE 标准浮点的机器。而且我还猜想你并不担心 NaN,因为 NaN 并未完全由 IEEE 754-1985 标准化,并且出现了两种相反的实现:HP 和 MIPS,几乎所有其他人都支持。1
有了这些限制,您如何才能在计算中获得可变性?
(1) 如果代码正在被并行化。确保没有发生这种情况。(这不太可能,但有些机器可能。)并行化是 FP 中结果变化的主要来源。至少我认识的一家公司,关心可复制性和并行性,拒绝使用 FP,只使用整数。
(2) 确保机器设置正确。
例如,大多数机器以 32 或 64 位精度计算(C 原始标准到处都是 64 位“双精度”。但 Intel x86/x87 可以在寄存器中以 80 位计算,并且在溢出时舍入到 64 或 32。1显示了如何更改x86/x87 精度控制从 80 位到 64 位,使用内联汇编。请注意,此代码是汇编级别的,不可移植 - 但大多数其他机器已经以 32 或 64 位精度进行计算,您无需担心x87 80 位。
(顺便说一句,在 x86 上,您只能通过使用 SSE FP 来避免所有问题;旧的传统 Intel x87 FP 永远无法给出完全相同的答案(尽管如果您将精度控制 (PC) 设置为 64 位而不是 80 位,你会得到相同的结果,除非有中间溢出,因为指数宽度不受影响,只是尾数))
例如,确保您在所有机器上使用相同的下溢模式。即确保 denorms 或启用,或者相反,所有机器都处于刷新到零模式。这是 Dobson 的选择:清零模式不是标准化的,但是一些机器,例如 GPU,根本就没有非规范化的数字。即许多机器有 IEEE 标准编号 FORMATS,但没有实际的 IEEE 标准算术(带 denorms)。我的想法是要求 IEEE denorms,但如果我绝对偏执,我会使用 flush 到零,并在软件中强制自己刷新。
(3) 确保您使用相同的语言选项。较旧的 C 程序以“双精度”(64 位)进行所有计算,但现在允许以单精度计算。无论如何,您希望在所有机器上都以相同的方式进行操作。
(4)一些较小的项目写你的代码:
避免编译器可能重新排列的大表达式(如果它没有正确实现严格的 FP 切换)
可能以简单的形式编写所有代码,例如
double a = ...;
double b = ...;
double c = a *b;
double d = ...;
double e = a*d;
double f = c + e;
而不是
f = (a*b) + (a*c);
这可能会优化为
f = a*(b+c);
我将在最后讨论编译器选项,因为它更长。
如果你做了所有这些事情,那么你的计算应该是绝对可重复的。IEEE 浮点数是精确的——它总是给出相同的答案。正是编译器在通向 IEEE FP 的过程中重新安排了计算,从而引入了可变性。
您应该不需要四舍五入低位。但这样做也不会受到伤害,并且可能会掩盖一些问题。请记住:您可能需要为每个添加屏蔽至少一个位......
(2) 编译器优化在不同机器上以不同方式重新排列代码。正如一位评论者所说,使用您的编译器开关来实现严格的 FP。
您可能必须禁用包含您的 sin 代码的文件的所有优化。
您可能必须使用挥发物。
希望有更具体的编译器开关。例如对于 gcc:
-ffp-contract=off --- 禁用融合乘法加法,因为并非所有目标机器都可能拥有它们。
-fexcess precision=standard --- 在内部寄存器中禁用 Intel x86/x87 超精度
-std=c99 --- 指定相当严格的 C 语言标准。不幸的是没有完全实现,因为我今天谷歌它
确保您没有启用优化,例如 -funsafe-math 和 -fassociativbe-math