Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Fast sign in C++ float...are there any platform dependencies in this code?

Searching online, I have found the following routine for calculating the sign of a float in IEEE format. This could easily be extended to a double, too.

// returns 1.0f for positive floats, -1.0f for negative floats, 0.0f for zero
inline float fast_sign(float f) {
    if (((int&)f & 0x7FFFFFFF)==0) return 0.f; // test exponent & mantissa bits: is input zero?
    else {
        float r = 1.0f;
        (int&)r |= ((int&)f & 0x80000000); // mask sign bit in f, set it in r if necessary
        return r;
    }
}

(Source: ``Fast sign for 32 bit floats'', Peter Schoffhauzer)

I am weary to use this routine, though, because of the bit binary operations. I need my code to work on machines with different byte orders, but I am not sure how much of this the IEEE standard specifies, as I couldn't find the most recent version, published this year. Can someone tell me if this will work, regardless of the byte order of the machine?

Thanks, Patrick

like image 588
Patrick Niedzielski Avatar asked Dec 12 '22 23:12

Patrick Niedzielski


2 Answers

How do you think fabs() and fabsf() are implemented on your system, or for that matter comparisons with a constant 0? If it's not by bitwise ops, it's quite possibly because the compiler writers don't think that would be any faster.

The portability problems with this code are:

  1. float and int might not have the same endianness or even the same size. Hence also, the masks could be wrong.
  2. float might not be IEEE representation
  3. You break strict aliasing rules. The compiler is allowed to assume that a pointer/reference to a float and a pointer/reference to an int cannot point to the same memory location. So for example, the standard does not guarantee that r is initialized with 1.0 before it is modified in the following line. It could re-order the operations. This isn't idle speculation, and unlike (1) and (2) it's undefined, not implementation-defined, so you can't necessarily just look it up for your compiler. With enough optimisation, I have seen GCC skip the initialization of float variables which are referenced only through a type-punned pointer.

I would first do the obvious thing and examine the emitted code. Only if that appears dodgy is it worth thinking about doing anything else. I don't have any particular reason to think that I know more about the bitwise representation of floats than my compiler does ;-)

inline float fast_sign(float f) {
    if (f > 0) return 1;
    return (f == 0) ? 0 : -1;
    // or some permutation of the order of the 3 cases
}

[Edit: actually, GCC does make something of a meal of that even with -O3. The emitted code isn't necessarily slow, but it does use floating point ops so it's not clear that it's fast. So the next step is to benchmark, test whether the alternative is faster on any compiler you can lay your hands on, and if so make it something that people porting your code can enable with a #define or whatever, according to the results of their own benchmark.]

like image 94
Steve Jessop Avatar answered Dec 15 '22 13:12

Steve Jessop


Don't forget that to move a floating point value from an FPU register to an integer register requires a write to RAM followed by a read.

With floating point code, you will always be better off looking at the bigger picture:

Some floating point code
Get sign of floating point value
Some more floating point code

In the above scenario, using the FPU to determine the sign would be quicker as there won't be a write/read overhead1. The Intel FPU can do:

FLDZ
FCOMP

which sets the condition code flags for > 0, < 0 and == 0 and can be used with FCMOVcc.

Inlining the above into well written FPU code will beat any integer bit manipulations and won't lose precision2.

Notes:

  1. The Intel IA32 does have a read-after-write optimisation where it won't wait for the data to be committed to RAM/cache but just use the value directly. It still invalidates the cache though so there's a knock-on effect.
  2. The Intel FPU is 80bits internally, floats are 32 and doubles 64, so converting to float/double to reload as an integer will lose some bits of precision. These are important bits as you're looking for transitions around 0.
like image 41
Skizz Avatar answered Dec 15 '22 13:12

Skizz