asin 使用 Clang 在不同的平台上产生不同的答案

asin produces different answers on different platforms using Clang

#include <cmath>
#include <cstdio>

int main() {
    float a = std::asin(-1.f);
    printf("%.10f\n", a);
    return 0;
}

我 运行 以上代码在使用 clang、g++ 和 Visual studio 的多个平台上运行。他们都给了我相同的答案:-1.5707963705

如果我在 macOS 上使用 clang 运行 它会给我 -1.5707962513。 macOS 上的 Clang 应该使用 libc++,但 macOS 是否有自己的 libc++ 实现?

如果我 运行 clang --verison 我得到:

Apple LLVM version 10.0.0 (clang-1000.11.45.5)
Target: x86_64-apple-darwin18.0.0
Thread model: posix
InstalledDir: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin

asin(-1) 的数学精确值将是 -pi/2,这当然是不合理的,不可能精确表示为 floatpi/2的二进制数以

开头
1.1001001000011111101101010100010001000010110100011000010001101..._2

您的前三个库将其(正确地)舍入为

1.10010010000111111011011_2 = 1.57079637050628662109375_10

在 MacOS 上它似乎被截断为:

1.10010010000111111011010_2 = 1.57079625129699707031250_10

这是小于1 ULP(单位在最后一位)的错误。这可能是由不同的实现引起的,或者您的 FPU 设置为不同的舍入模式,或者在某些情况下,编译器可能会在编译时计算值。

我不认为 C++ 标准真的对超越函数的准确性提供任何保证。 如果您的代码确实依赖于(platform/hardware 独立的)准确性,我建议使用库,例如​​ MPFR。否则,只能忍受差异。或者查看在每种情况下调用的 asin 函数的源代码。

asin是在libm中实现的,它是标准C库的一部分,不是C++标准库。 (从技术上讲,C++ 标准库包括 C 库函数,但实际上 Gnu 和 LLVM C++ 库实现都依赖于底层平台数学库。)三个平台——Linux、OS X和 Windows -- 每个都有自己的数学库实现,所以如果使用库函数,它肯定是不同的库函数,结果可能在最后一位位置不同(这是你的测试显示)。

但是,很可能在所有情况下都不会调用库函数。这将取决于编译器和您传递给它们的优化选项(可能还有其他一些选项)。由于 asin 函数是标准库的一部分,因此具有已知行为,因此编译器在编译时计算 std::asin(-1.0F) 的值是完全合法的,就像任何其他常量表达式一样(像 1.0 + 1.0,几乎所有编译器都会在编译时常量折叠到 2.0)。

因为你没有提到你正在使用什么优化设置,所以很难准确地说出发生了什么,但我用 http://gcc.godbolt.org 做了一些测试以获得一个基本的想法:

  • GCC 常量在没有任何优化标志的情况下折叠对 asin 的调用,但它不会预先计算 printf 中的参数提升(将 a 转换为 double 以便将其传递给 printf)除非您至少指定 -O1。 (使用 GCC 8.3 测试)。

  • Clang (7.0) 调用标准库函数,除非您指定至少 -O2。但是,如果您显式调用 asinf,它会在 -O1 处折叠。去图吧。

  • MSVC (v19.16) 不固定折叠。它要么调用 std::asin 包装器,要么直接调用 asinf,具体取决于优化设置。我不太明白wrapper是干什么的,也没花太多时间研究。

GCC 和 Clang 常量都将表达式折叠为完全相同的二进制值(0xBFF921FB60000000 作为双精度值),即二进制值 -1.10010010000111111011011(尾随零被截断)。

请注意,三个平台上的 printf 实现也存在差异(printf 也是平台 C 库的一部分)。理论上,您可以从相同的二进制值看到不同的十进制输出,但是由于 printf 的参数在调用 printf 之前被提升为 double 并且提升是精确定义的,而不是值改变,这在这种特殊情况下极不可能产生任何影响。

附带说明一下,如果您真的很在意小数点后第七位,请使用 double 而不是 float。实际上,您应该只在精度不重要的非常具体的应用程序中使用 float;正常的浮点类型是 double.