Aussie AI
Pitfalls and Portability
-
Book Excerpt from "Generative AI in C++"
-
by David Spuler, Ph.D.
Pitfalls and Portability
Bitwise manipulation of float data is not the most portable code in the world. Let's examine some of the possible pitfalls in using these techniques.
Bitwise zero testing:
If you've gone to the trouble to access the bits of a floating-point number,
you might as well use them.
Obviously, testing for “0.0
” is a common requirement, so let's make it faster:
#define FLOAT_IS_ZERO(f) \ ((*reinterpret_cast<unsigned int*>(&f)) == 0u) // Bug!
Oops! We forgot about negative zero. There are two zeros in floating-point, depending on the sign bit, and it's hard to test it efficiently with bitwise operations (e.g. mask the sign bit or shift left first).
Strict anti-aliasing rule.
An important point about all this is that most of it is platform-dependent,
and officially “undefined behavior”.
Some of it is standardized by IEEE 754, but many variations are possible.
Another issue is that there's a “strict anti-aliasing rule” that specifies that many of these tricks
are officially non-standard methods.
Accessing a floating-point number as if it's an unsigned number is a technical violation of this rule.
The “reinterpret_cast
” method is probably less likely to run afoul of this problem,
but it's still not guaranteed.
Anyway, the union
trick and the use of memcpy
don't really strike me as being particularly more portable,
although memcpy
might be less likely to be optimized wrongly by a compiler making wrong assumptions.
Some additional risk mitigations are warranted, such as adding a lot of unit tests of even the most basic arithmetic operations.
However, you're still not officially covered against an over-zealous optimizer that might rely on there being no aliases allowed.
Byte sizes. Another much simpler portability issue is checking the byte sizes of data types, which can vary across platforms. Most of this bit-fiddling stuff relies on particular 16-bit and 32-bit layouts. It doesn't hurt to add some self-tests to your code so you don't get bitten on a different platform, or even by a different set of compiler options:
yassert(sizeof(int) == 4); yassert(sizeof(short int) == 2); yassert(sizeof(float) == 4); yassert(sizeof(unsigned int) == 4);
Also note that for this to work well, both types must be the same size. So, this would be a useful code portability check if it worked:
#if sizeof(float) != sizeof(unsigned int) // Fails! #error Big blue bug #endif
This macro preprocessor trick doesn't work because sizeof
isn't allowed in a preprocessor expression,
because the preprocessing phase precedes the syntax analysis.
A better version uses a “static_assert
” statement,
which does compile-time checking in a more powerful way.
static_assert(sizeof(float) == sizeof(unsigned), "Bug!");
• Next: • Up: Table of Contents |
The new AI programming book by Aussie AI co-founders:
Get your copy from Amazon: Generative AI in C++ |