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
,这当然是不合理的,不可能精确表示为 float
。 pi/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
.
#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
,这当然是不合理的,不可能精确表示为 float
。 pi/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
.