wait...what? Is this real?
wait...what? Is this real?
1. Yes -- On old x86 systems, the x87 registers were 80-bits and the bottom bits were undefined. You could theoretically have a 1-bit difference on some results depending on what the bottom 26 bits (that were undefined, because you had 64-bit floats most of the time, even though the machine did things 80-bits at a time).
2. No -- Modern x86 systems use SSE registers, which are 64-bits. There are a whole slew of configuration options, but if Windows / Linux does their job, you should have the same rounding-errors across your program (unless you manually change the rounding options, but that's your own fault if you go that route).
3. Kinda yes -- Floating point operations are NON-associative. (A+B)+C does NOT equal A+(B+C). The best example is Python:
>>> (1+1)+2.053 9007199254740994.0 >>> 1+(1+2.053) 9007199254740992.0
253 is the first value which starts to "round off" 1.0 in double-precision. So (1+2.053) == 2.053, because the value was rounded off.
Due to #3, even if you did everything correctly (ie: set the rounding flags so they were consistent), if your arithmetic happened in slightly different orders (very common in network-games, where different players may have their updates in slightly different orders), you end up with a 1-bit error between clients... which completely borks your simulation.
Because #3 is nonintuitive, many people think that you get different results when using floats. But its simply due to non-associativity.
>>> (1+1)+2.0**53
9007199254740994.0
>>> 1+(1+2.0**53)
9007199254740992.0
The exponent operator (double star) got transformed into italics apparently.The newlines also helped, by the way, because I was wondering what putting the two numbers next to each other was meant to indicate.
I spent a long time once debugging an issue arising from this. Code was basically:
double x = y;
[...]
if (x>y) fail;
Nothing in between the assignment and the check changed the value of x or y, yet the check triggered the fail condition. Turned out to be that one of the value stayed in the 80 bit x87 register and one was written to RAM as a 64 bit value, then loaded back into an x87 register for the check, resulting in the inequality. Running in a debugger wrote both values out of the registers before the check, making the problem unreproducible.What was the check for?
(Even during the FPU days, every ; should have rounded each value to the size of the variables, unless you use a special compiler switch to make FP math faster)
Modern applications and compilers largely do not use x87, but they can, and x87 results depending on hidden bits persists in modern x86_64 CPUs.
Are your sure of this? It's my understanding that SSE registers require the use of a special API and standard floating point operations do not use them.
But the last time I worked with them was writing a SIMD vector library 7 years ago.
-mfpmath=unit
Generate floating-point arithmetic for selected unit unit. The choices for unit are:
‘387’
Use the standard 387 floating-point coprocessor present on the majority of chips and emulated otherwise. Code compiled with this option runs almost everywhere. The temporary results are computed in 80-bit precision instead of the precision specified by the type, resulting in slightly different results compared to most of other chips. See -ffloat-store for more detailed description.
This is the default choice for non-Darwin x86-32 targets.No, SSE registers are used by most compilers that do floating point these days. They support all the usual IEEE float math operations and a host of bit twiddling operations as well as vector operations. The vector operations do still require using compiler intrinsics in C++, although some autovectorization does occur in gcc, icc, and llvm.
For 32-bit systems, you still theoretically have to check, but I think every halfway modern compiler also uses SSE2 as the default target.
This is a good argument for stricter typechecking than anything else. In Rust, a condition like that wouldn't compile because of the mixed types. That's probably a good call in my opinion: it alerts the programmer that there's something funky going on, and it forces the programmer to be explicit about how the comparison should actually happen.
I do find myself doing the "cargo cult" thing from time to time. I was working on a codebase where Lua was embedded in C++, and there were all these flags that had to work in both systems. Because Lua only supports 64-bit floats as a numeric type, the natural type for these were doubles, and "by convention" they only ever had the values 0.0 or 1.0. It was a bit silly of me, but everytime i saw the line `if (fSomeFlag == 1.0f) ...` in the code, I would wince. It just feels wrong to do that to floats. I found myself regularly writing `if (fSomeFlag > 0.5f) ...` instead.
Of course, I was wrong about this. These weren't values determined as a result of arithmetic, they were always set to whole numbers. There was never going to be a case where the actual value of the flag would be 0.9999998f or whatever. But still: once you've been burned by the "floating point equality comparison", you live in fear of it forever after.
My point was that many researchers can only cobble together somewhat barely working software by taking snippets from their undergraduate textbooks (or googling/stack overflowing, for those under 40), and have no idea about many/most of the underlying principles. If only they would recognize that, and leave the software development to professionals, instead of treating it as an inconsequential implementation detail that is beneath them. Then again, if they did, they wouldn't have to pay me what they do to fix it up afterwards. So meh?
Knock, knock. Who is there? Floating-point contraction.
* (+ (+ 1 1) (expt 2 256))
115792089237316195423570985008687907853269984665640564039457584007913129639938
* (+ 1 (+ 1 (expt 2 256)))
115792089237316195423570985008687907853269984665640564039457584007913129639938The point is to demonstrate a property of IEEE floats, not the language. I'd expect CL has some means of using IEEE floats too, and with the appropriate syntax, the same issues could be demonstrated.
This is due to the fact that several pipelines will do calculations in parallel and depending on which pipeline ends first (which can depend on many factors like current temperature) the sums can happen in a different order, leading to different rounding results.
$ python3
>>> s = [10**-x for x in range(16)]
>>> s
[1, 0.1, 0.01, 0.001, 0.0001, 1e-05, 1e-06, 1e-07, 1e-08, 1e-09, 1e-10, 1e-11, 1e-12, 1e-13, 1e-14, 1e-15]
>>> sum(s)
1.1111111111111112
>>> sum(reversed(s))
1.111111111111111
(it uses CPU for simplicity, but GPU has exactly the same math)Somewhere I've seen a little routine (by Stefan Karpinski of Julia fame) that starts with a fixed pre-determined array of numbers, and you give it a desired target number, and it permutes the numbers of the array such that when you sum them up (naively, from left to right) the result is your desired target number [2].
https://discourse.julialang.org/t/accurate-summation-algorit...
EDIT: [2] https://discourse.julialang.org/t/array-ordering-and-naive-s...
Sorting the numbers is necessary to minimize cancellation error. But absolute minimum cancellation error can only be done sequentially: the parallel version may return different results.
l = list(s)
l.sort()
sum(l)
l = list(reversed(s))
l.sort()
sum(l)
Both return 1.111111111111111 now. Sorting is the "synchronization" step you need to get all your parallel tasks back into a defined order.Parallel systems have great difficulty in defining an order of operations. Floating-point numbers are non-associative, so order matters for bitwise compatibility.
With that being said, you CAN define an order, and it CAN be done in parallel. The parallel-prefix sum is a fully defined order of operations for example, and if you sort all numbers (from smallest to largest), you'll get a more accurate result.
https://en.wikipedia.org/wiki/Prefix_sum#Algorithm_1:_Shorte...
The + operations always happen in the same order, even on a GPU. So you should get the same results every time with floating-point math. But it is non-intuitive for many people to work with non-associative floating-point math.
-----------
Note that writing a sequential algorithm with fully defined order is still grossly difficult! IMO, if anyone wants a bit-accurate simulation, use 64-bit integers instead.
If you absolutely must use floats, then define a proper order of all operations, and include sorting the numbers (!!) to force a fully defined order as much as possible. You may need to sort your pool of numbers many, many times per calculation. But that's what you have to do to get a defined ordering.
One thing I've never really been sure about is how context-switching affects this. I've always assumed this could also be a source of non-deterministic floating-point results: when the OS decides to switch threads, it has to save the CPU & FPU state, and restore it when switching back. Since context switches are (practically speaking) non-deterministic on a pre-emptive OS, this would imply truncating 80-bit to 64-bit floating point values could also happen on non-deterministic points in time.
Does anyone know if this is actually true? Or do the full 80-bits get saved/restored on context switches?
The FPU state is saved using the F(N)SAVE x86 instruction, which stores 108 bytes to memory, which contains all visible FPU state.