Deterministic simulation using floats?

Started by
26 comments, last by ddyer 15 years, 9 months ago
Quote:Original post by Cygon
I'd put my money on IEEE 754. Most platforms follow this standard, so you'll have optimal speed on them. You can worry about emulation when you actually need to port your game to obscure platform XY, but I doubt that need will ever arise.
It doesn't take obscure hardware. The classic x87 FPU is one such oddball platform as it uses an 80-bit floating point format internally, and few other platforms do as well such as the Itanium. So for code to be consistent the result of every operations has to be spilled to memory to truncate the result (or SSE/3DNow! needs to be used throughout). The compiler will also have to disable most floating-point optimizations, such as reordering associative expressions, replacing constant division by reciprocal multiplication or otherwise exploiting algebraic identities. Now as far as I can tell the "/fp:strict" option in recent MSVC versions ought to handle this, though I have to admit that the documentation is still somewhat unclear on whether you can expect full consistency across platforms.
In addition to needing working support for strict math on all compilers involved (not something I would expect of immature console tools) you'll have to pray that there aren't any bugs in the implementations (such as the Pentium FDIV bug, or Pentium II/Pro FIST bug), and that they've actually set out to be IEEE compliant in the first place and not save cycles and silicon with non-standard flush-to-zero behavior and such.
Furthermore you most definitely can't expect any support math functions beyond the core operations to be consistent so you'll have to provide your own methods for everything from pow() to formatted IO.
Emulators almost certainly won't be able to play the game either but whether that's a good thing or a bad thing is debatable. I suppose the most likely negative scenario is a attempting to play on backwards-compatible consoles or some retro gamer in ten years from now when we've all moved on to fancy new 1024-bit über-processors.

Developing and debugging a deterministic simulation is hard enough without having to worry about floating-point consistency. The last thing you want is to have to worry about some unforeseen issue cropping up towards the end of development. So do yourself a favor and stick with fixed-point arithmetic for the game logic, or at least don't expect networking to work between different platforms.

By the way has anyone successfully managed to use floats in a multi-platform RTS?
Advertisement
Would Fixed Point integer maths should be OK? Say 32Bit for integers and 16bit for fractions?

Is there a GOOD (i.e. not the first one on Google) fixed point maths library?

There also must be a fast way to convert back to a float/double?!?
Quote:Original post by implicit
Developing and debugging a deterministic simulation is hard enough without having to worry about floating-point consistency. The last thing you want is to have to worry about some unforeseen issue cropping up towards the end of development. So do yourself a favor and stick with fixed-point arithmetic for the game logic.

QFE. Fixed-point isn't too tricky once you've got the idea behind it and a handful of functions to do the dirty work, and you'll save yourself a whole load of headaches.
Quote:Original post by ROBERTREAD1
Would Fixed Point integer maths should be OK? Say 32Bit for integers and 16bit for fractions?

Is there a GOOD (i.e. not the first one on Google) fixed point maths library?

There also must be a fast way to convert back to a float/double?!?


Converting back to a double/float is easy.

struct fixed_16_16 {  int raw;  // operator += and -= are trivial  fixed_16_16& operator*=( fixed_16_16 const& other ) {    __int64 raw_ = raw;    raw_ *= other;    raw_ >> 16;    raw = raw_;    return *this;  }  fixed_16_16& operator/=( fixed_16_16 const& other ) {    __int64 a,b;    a = raw;    b = other.raw;    a << 32;    a /= b;    a >> 16;    raw = (int)a;    return *this;  }  fixed_16_16(int whole):raw(whole<<16){};  explicit fixed_16_16(double value); // hmm, to do  // copy constructors, operator swap, etc  static fixed_16_16 fractional_part(int fraction) {    fixed_16_16 retval;    retval.raw = fraction;    return retval;  }  int whole_part() const;  int round_up() const;  int round_down() const;  double approx_value() const;};// non-member operator* and operator/ in terms of *= and /= go here


Do you have room for a 32/32 bit fixed point, or is 16/16 enough? :)
Quote:Original post by Hodgman
Floats can be represented differently in CPU registers than in RAM, which means that errors may be introduced when moving between these two types of memory.

Say what? I get the feeling that you think the scheduler uses FLD and FST on a context switch to save/restore state. It does not. It uses FSAVE/FRSTOR, which preserve the full internal precision.
Quote:Original post by Hnefi
Quote:Original post by Hodgman
Floats can be represented differently in CPU registers than in RAM....
That sounds highly unlikely ... Every floating point number has one and exactly one representation in computer memory - one and only one sequence of bits.
It's fairly common for them to be 32/64-bit in memory and 80-bit in the CPU, which to me implies that at from 16 to 48 bits of precision will be lost when copying from registers to RAM:
Quote:Original post by implicit
The classic x87 FPU is one such oddball platform as it uses an 80-bit floating point format internally, and few other platforms do as well such as the Itanium.


Quote:Original post by Sneftel
I get the feeling that you think the scheduler uses FLD and FST on a context switch to save/restore state. It does not. It uses FSAVE/FRSTOR, which preserve the full internal precision.
Thanks for the correction. So the bitwise representation (i.e. all 80 bits) *will* be preserved during a context switch?

But, it's still possible for the CPU to use a different internal representation than what will be stored in RAM? Meaning that the value of the boolean value z in my previous code snippet still depends on how much code is inserted at "..."? (i.e. if there's just enough code to force the compiler to move one (and only one) of the variables back to RAM temporarily then it may be false)
Yes. That means that on subsequent runs of your program on identical hardware, the results should always be the same.

But if you recompile your code (eg. with slightly different optimisation settings), then the results before recompilation could be different depending on how the compiler issued/ordered instructions.
NextWar: The Quest for Earth available now for Windows Phone 7.
Quote:Original post by HodgmanIt's fairly common for them to be 32/64-bit in memory and 80-bit in the CPU, which to me implies that at from 16 to 48 bits of precision will be lost when copying from registers to RAM:
Quote:Original post by implicit
The classic x87 FPU is one such oddball platform as it uses an 80-bit floating point format internally, and few other platforms do as well such as the Itanium.


Quote:Original post by Sneftel
I get the feeling that you think the scheduler uses FLD and FST on a context switch to save/restore state. It does not. It uses FSAVE/FRSTOR, which preserve the full internal precision.
Thanks for the correction. So the bitwise representation (i.e. all 80 bits) *will* be preserved during a context switch?

But, it's still possible for the CPU to use a different internal representation than what will be stored in RAM? Meaning that the value of the boolean value z in my previous code snippet still depends on how much code is inserted at "..."? (i.e. if there's just enough code to force the compiler to move one (and only one) of the variables back to RAM temporarily then it may be false)

C/C++ is not assembler. Unless your compiler says otherwise, a float is 32 bits and a double 64 bits. The specialized 80-bit FPU registers on some platforms probably deal with such variables by padding or similar techniques. To make use of 80 bit floats, you need to specify that explicitly - typically by assigning a long double variable. At least, that's my understanding.

According to Wikipedia, the 82 bit registers on the Itanium are also used to "preserve precision for intermediate results". I'd guess that this means that the extra 2 bits are used to reduce rounding errors when performing multiple calculations in sequence, but this should definitely not have any effect when simply copying a number.
-------------Please rate this post if it was useful.
Quote:Original post by Hnefi
C/C++ is not assembler. Unless your compiler says otherwise, a float is 32 bits and a double 64 bits. The specialized 80-bit FPU registers on some platforms probably deal with such variables by padding or similar techniques. To make use of 80 bit floats, you need to specify that explicitly - typically by assigning a long double variable. At least, that's my understanding.

According to Wikipedia, the 82 bit registers on the Itanium are also used to "preserve precision for intermediate results". I'd guess that this means that the extra 2 bits are used to reduce rounding errors when performing multiple calculations in sequence, but this should definitely not have any effect when simply copying a number.

Yes, my compiler says a float is 32/64bits... in RAM.
The compiler tells the CPU to load a 32/64bit float from RAM ... then once the CPU fetches that data *the CPU* can do whatever it wants, such as converting the 32/64bit value an intermediate 82bit representation. The compiler may be completely oblivious that this is happening.

A padded 32-bit value won't work in an 82-bit register, so I assume 32bit values are either always converted to higher-precision, or that the CPU must also have a bunch of 32-bit registers lying around as well as the new 82-bit ones.

They way that I interpret "The floating point registers are [>64] bits long to preserve precision for intermediate results" is that the internal representation is more precise than the in-memory representation. Hence moving data from intermediate storage (registers) back to RAM will cause a loss of accuracy.
It depends on how the FPU is implemented. Since it must be able to differentiate between 32 and 64 bit floating point representations anyway - which means it must be able to deal with differently sized mantissas and exponents without affecting the accuracy of either representation - it is not unreasonable to expect the processor to only use the full 80/82 bits when given a long double (or equivalent) variable. Whether the Itanium actually behaves this way, I don't know. I don't have one available at the moment.
-------------Please rate this post if it was useful.

This topic is closed to new replies.

Advertisement