3
#include <cmath>
#include <cstdio>

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

I ran the code above on multiple platforms using clang, g++ and Visual studio. They all gave me the same answer: -1.5707963705

If I run it on macOS using clang it gives me -1.5707962513. Clang on macOS is supposed to use libc++, but does macOS has its own implementation of libc++?

If I run clang --verison I get:

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
Samantha
  • 284
  • 1
  • 11
  • 1
    The first 7 digits look identical. Isn't that sufficient? – Scheff's Cat Mar 19 '19 at 16:09
  • There was once a similar Q/A: [SO: Precision of acos function in c++](https://stackoverflow.com/a/32414939/7478597). – Scheff's Cat Mar 19 '19 at 16:10
  • 1
    The difference could be from the code that converts the value to text, or it could be in the low bits of the result. Either way, since `float` supports 6 digits, those differences are deep down in the noise. (Formally, `float` is required to support **at least** six digits; typical implementations just do six) – Pete Becker Mar 19 '19 at 16:10
  • Well it is, I just think its strange that they are different. The implementation should be the same when both the compiler and the standard library implementation is? – Samantha Mar 19 '19 at 16:12
  • 1
    What is the question? Why is the difference there? Or is this a bug? – geza Mar 19 '19 at 16:12
  • 2
    It might depend whether it's computed in library or directed to a CPU command. I once read a disturbing story about H/W sqrt on Intel CPUs vs. S/W sqrt(). – Scheff's Cat Mar 19 '19 at 16:14

2 Answers2

4

asin is implemented in libm, which is part of the standard C library, not the C++ standard library. (Technically, the C++ standard library includes C library functions, but in practice both the Gnu and the LLVM C++ library implementations rely on the underlying platform math library.) The three platforms -- Linux, OS X and Windows -- each have their own implementation of the math library, so if the library function were being used, it could certainly be a different library function and the result might differ in the last bit position (which is what your test shows).

However, it is quite possible that the library function is never being called in all cases. This will depend on the compilers and the optimization options you pass them (and maybe some other options as well). Since the asin function is part of the standard library and thus has known behaviour, it is entirely legitimate for a compiler to compute the value of std::asin(-1.0F) at compile time, as it would for any other constant expression (like 1.0 + 1.0, which almost any compiler will constant-fold to 2.0 at compile time).

Since you don't mention what optimization settings you are using, it's hard to tell exactly what's going on, but I did a few tests with http://gcc.godbolt.org to get a basic idea:

  • GCC constant folds the call to asin without any optimisation flags, but it does not precompute the argument promotion in printf (which converts a to a double in order to pass it to printf) unless you specify at least -O1. (Tested with GCC 8.3).

  • Clang (7.0) calls the standard library function unless you specify at least -O2. However, if you explicitly call asinf, it constant folds at -O1. Go figure.

  • MSVC (v19.16) does not constant fold. It either calls a std::asin wrapper or directly calls asinf, depending on optimisation settings. I don't really understand what the wrapper does, and I didn't spend much time investigating.

Both GCC and Clang constant fold the expression to precisely the same binary value (0xBFF921FB60000000 as a double), which is the binary value -1.10010010000111111011011 (trailing zeros truncated).

Note that there is also a difference between the printf implementations on the three platforms (printf is also part of the platform C library). In theory, you could see a different decimal output from the same binary value, but since the argument to printf is promoted to double before printf is called and the promotion is precisely defined and not value-altering, it is extremely unlikely that this has any impact in this particular case.

As a side note, if you really care about the seventh decimal point, use double instead of float. Indeed, you should only use float in very specific applications in which precision is unimportant; the normal floating point type is double.

rici
  • 234,347
  • 28
  • 237
  • 341
3

The mathematically exact value of asin(-1) would be -pi/2, which of course is irrational and not possible to represent exactly as a float. The binary digits of pi/2 start with

1.1001001000011111101101010100010001000010110100011000010001101..._2

Your first three libraries round this (correctly) to

1.10010010000111111011011_2 = 1.57079637050628662109375_10

On MacOS it appears to get truncated to:

1.10010010000111111011010_2 = 1.57079625129699707031250_10

This is an error of less than 1 ULP (unit in the last place). This could be caused either by a different implementation, or your FPU is set to a different rounding mode, or perhaps in some cases the compiler computes the value at compile-time.

I don't think the C++ standard really gives any guarantees on the accuracy of transcendental functions. If you have code which really depends on having (platform/hardware independent) accuracy, I suggest to use a library, like e.g., MPFR. Otherwise, just live with the difference. Or have a look at the source of the asin function which is called in each case.

chtz
  • 17,329
  • 4
  • 26
  • 56