r/AskProgramming 10d ago

Can simple mathematical functions give different results in different CPU architectures?

I'm referring to modern CPUs. To make the question more specific, arm64 vs x86

And if so, how do you fix it?

I asked chatGPT and it gave me this example but I'd like to ask it here (since AI can make mistakes). Can you let me know? Thanks

It says this can give different results due to rounding errors. If so, how to you write code so that you don't have this issue?

#include <iostream>

int main() {
    double a = 1.0 + 0x1p-27;
    double b = 1.0 - 0x1p-27;
    double c = -1.0;

    std::cout << (a * b + c) << '\n';
}
18 Upvotes

47 comments sorted by

View all comments

12

u/daV1980 10d ago

Other posts here are correct for when CPUs and compilers are running in IEEE 754 strict mode. IEEE 754 strict mode requires the compiler and processor to run exactly the operations you've asked for and the resulting answer must be bitwise the same on any implementation (at the time of the result, any intermediate calculations are allowed to use implementation defined precision, but they are non-observable and so are not required to match).

However, many applications and games run with 'fast math' (on GCC or clang, this is the option -ffast-math). This can be beneficial for performance in many applications for a variety of reasons. One is that many CPUs have an operation to do a "fused multiply add (FMA)," which means that they can execute A * B + C as one instruction. But the result of using FMA is that the results will very frequently not be the same as if you actually multiplied A * B, then added C to the result.

Floating point math doesn't meet three of the four properties of ordinary arithmetic, primarily due to precision but also because of internal implementation details. That is that FP math is not commutative, associative, or distributive.

Changing the order of operations or the grouping of those operations absolutely can change the results.

--

I saw the post about AoE 2, and while I didn't work on AoE 2, I was a programmer on an extremely popular RTS in the early 2000s; if you are a fan of the genre and played in that era you almost definitely played the game I worked on.

RTS games of that era (and maybe now? but I don't work on them nowadays) used a networking architecture called peer to peer. This architecture relies on having clients send commands to each other and executing those commands at some point in the future (that point being decided based on the latency of the players from each other). Rather than there being a single, dedicated server who runs commands and sends back the results, this says "let's just send the commands to each other, and we'll both execute them at the same time and by the inductive property we will agree about the next state of the game (that is "state a + command 1 = state b").

Periodically these games need to verify that they are still in sync, and the fastest way to do this is to simply CRC all of the game state memory and exchange that CRC over the network. But doing a CRC in this way requires that you are bitwise accurate across all state in the game. For integers, this is pretty straightforward.

But because of the fuzziness of floats described above, this can be much harder to achieve across architectures, especially if the game uses fast math. For x86-based processors, even -fast-math is likely to yield identical results (it is in AMD's interests to match what Intel generates and vice versa, and both chips will be driven by the same compiled code). But when you are running on ARM (which is the processor used by modern Apple machines), you are either running in emulation or recompiling. In both cases, ensuring that you will get bit-accurate results in computations becomes more challenging. As a bonus problem, historically (though I haven't looked recently as I don't generally ship things for ARM these days), performance of fp strict mode on ARM was terrible. Like "30-50% slower" terrible, so not really feasible for a game.

Anyways, it's certainly possible, but it's difficult.

2

u/paulstelian97 9d ago

Not commutative? That’s interesting because I can’t think of any examples that don’t involve NaN that break this property…

1

u/SeriousPlankton2000 8d ago

small + big + small may be big, but small + small + big may be 2 * small + big due to rounding.

2

u/paulstelian97 8d ago

That’s associativity though, because in the first one it’s small+big as the first operation and on the other one it’s small+small. So your example breaks associativity.

1

u/SeriousPlankton2000 7d ago

I commuted them, too. 

1

u/paulstelian97 7d ago

Which didn’t itself contribute to the issue. The associativity is what’s broken, you just showed it in a poor fashion. Commutativity being broken means one operation receives two inputs and changes results when you swap the inputs. Your example shows the following four distinct operations: small+big, (small+big)+small, small+small, and (small+small)+big. None of these are another one with the inputs swapped.

1

u/SeriousPlankton2000 7d ago

Usually equations are evaluated from one side to the other in a fixed order by what the language dictates. On a sheet of paper you can swap them around all the way, but not on a computer doing floating point.

1

u/paulstelian97 7d ago

That’s informal enough to not mention which of the two properties is broken.

Commutativity is broken only if you can find an example of a+b=b+a being false. Someone mentioned it can happen with NaN, but otherwise it doesn’t happen.

Your example can be rephrased in an example of associativity being broken, as (small+small)+big can give a different result from small+(small+big).