1

I have some cross platform code I'm working with. On the Mac it's compiled with Clang, on Windows it's compiled with Visual C++.

There is a calculation that can be sensitive, and there was a difference between Mac and Windows that was triggering asserts. It ends up there is a difference between acos results, but I'm not clear why.

On both platforms, the input to acos is exactly -1.0f. In Visual C++, acos(-1.0f) is 3.14159274. That's the value of pi as a float, which is what I'd expect.

But on macOS:

float value = acos(-1.0f);

...evaluates to 3.1415925. Thats just enough of an accuracy difference to trigger issues in the code. acos is an operation that can be imprecise with float - I understand that. And different compilers can have different implementations of acos. I'm just unclear why Clang seems to have issues with such a simple acos result while Visual C++ doesn't. A float is capable of representing 3.14159274, but that's not the result I'm getting.

It is possible to get an accurate/Visual C++ aligned value out of Xcode's version of Clang with:

float value = (float)acos((double)-1.0f);

So I can fix the issue by moving to higher accuracy, and then down casting the value back to float to preserve the same rounding as Windows. I'm just looking for a justification as to why the extra precision is necessary when the VC++ compiler doesn't seem to have a precision issue. It could be differences between the Clang/Xcode/VC++ math libraries as well. I just assumed that acos(-1.0) might be more settled across the compilers. I couldn't find any difference in round modes (even though rounding should not be necessary) and fresh projects in Xcode and Visual Studio show the same difference. Both machines are Intel.

Colin Cornaby
  • 741
  • 4
  • 14
  • 3
    Unrelated, but instead of `(double)-1.0f`, why not simply `-1.0`, which is guaranteed to be a `double`? If you ever feel the need to do a C-style cast in C++, then you should take that as a sign that you're probably doing something wrong. – Some programmer dude Aug 02 '22 at 06:16
  • I’d imagine the difference is actually between the processors rather than compilers, trigonometric functions have hardware instructions. – George Aug 02 '22 at 06:20
  • there are different ways to calculate `acos(1)` and how to implement it. There must not be a table that says `acos(1) == pi` but there can be one – 463035818_is_not_an_ai Aug 02 '22 at 06:20
  • 5
    There is generally no way to ensure bit-wise identical results for transcendental math functions across toolchains. If your code needs to run across multiple toolchains, you would want to adapt it to this reality. FWIW, `3.14159274` is greater than mathematical π (3.15159265...), and one *could* argue that `acos()` should never return a result that is mathematically "prohibited". Side remark: Are you sure you need `acos`? Some commonly encountered uses of `acos` can be replaced with algebraic computation. – njuffa Aug 02 '22 at 06:29
  • @George - The Intel Mac I have runs both Windows and macOS. Both VC++ and Xcode on the same machine produce different outputs for acos. Could still be a difference in what instructions the two compilers use - but same CPU is being used across both. – Colin Cornaby Aug 02 '22 at 06:50
  • @Someprogrammerdude - You're right that the casting is unnecessary. This was taken from existing code, and I removed the variables and replaced them with constants. In context, there is a variable inside the acos that needs to be cast to double. – Colin Cornaby Aug 02 '22 at 06:54
  • IEEE-754 doesn't require transcendental functions to be correctly rounded because that'll require far more memory and computational power [Floating point accuracy with different languages](https://stackoverflow.com/q/58411805/995714), [Why do sin(45) and cos(45) give different results?](https://stackoverflow.com/a/31509332/995714), [Why do I get platform-specific result for std::exp?](https://stackoverflow.com/q/54250126/995714), [Math precision requirements of C and C++ standard](https://stackoverflow.com/q/20945815/995714) – phuclv Aug 02 '22 at 09:19
  • Does this answer your question? [Math precision requirements of C and C++ standard](https://stackoverflow.com/questions/20945815/math-precision-requirements-of-c-and-c-standard) – phuclv Aug 02 '22 at 09:19
  • duplicates: [`asin` produces different answers on different platforms using Clang](https://stackoverflow.com/q/55245293/995714), [gcc and sin/cos/transcendental functions precision like in Windows](https://stackoverflow.com/q/15062728/995714) – phuclv Aug 02 '22 at 09:20
  • 1
    `Could still be a difference in what instructions the two compilers use` no, there are only transcendental instructions in x87 and they're [extremely](https://stackoverflow.com/q/55665744/995714) [slow](https://stackoverflow.com/q/12485190/995714) so no one uses them nowadays. C and C++ standard library implementations have their own faster software versions using the new SSE/AVX instruction sets – phuclv Aug 02 '22 at 09:29

1 Answers1

7

If you look at the binary representation of these floating point values you can see that the mac/clang's value A is the next lowest floating-point number than windows/msvc's value B

A    3.14159250    0x40490FDA
B    3.14159274    0x40490FDB

Whilst B is closest to the true value of π, it is actually greater than π as @njuffa points out in their comment.

Reading the specification, it looks like acosf is supposed to return a value in the closed range [0,π]. Technically A meets this criteria whilst B doesn't.

In summary -

  • A is the closest value to, but less than, π
  • B is the closest value to π

The difference in these may be as a result of a deliberate decision of the respective standard library implementors.

I'd also observe that both values are true inverses of cosf as both cosf(A) and cosf(B) equal -1.0f.

Generally speaking, though, it is unwise to rely on exact bit-level accuracy with any floating point calculations. If you are not already aware of it, the document What Every Computer Scientist Should Know About Floating-Point Arithmetic explains why.


Edit: I was curious and found what might be relevant Apple source code here.

Return value:
    ...       
    Otherwise:
       ...            
       Returns a value in [0, pi] (C 7.12.4.1 3).  Note that 
       this prohibits returning a correctly rounded value for acosf(-1),
       since pi rounded to a float lies outside that interval.
Ian Cook
  • 1,188
  • 6
  • 19
  • Thanks, this seems like the best answer - and the range requirement is possibly an explanation. This is existing code that was built using float, but this sounds like a justification to be either less picky about the float output, or move to double. – Colin Cornaby Aug 02 '22 at 15:01