5

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

Patrick Niedzielski
  • 1,194
  • 1
  • 8
  • 21
  • See this: http://stackoverflow.com/questions/1723575/how-to-perform-bitwise-operation-on-floating-point-numbers/1723938#1723938 – tiftik Mar 24 '10 at 14:40
  • Right, thanks. I should have spotted this; I just finished reading an article on 64-bit portability, which stressed type sizes... (^_^")> – Patrick Niedzielski Mar 24 '10 at 14:47
  • 2
    What makes you think you need to resort to a hack such as this ? Have you actually profiled your code and identified this operation as a significant bottleneck ? – Paul R Mar 24 '10 at 15:02
  • See the answer below, I have. – Patrick Niedzielski Mar 24 '10 at 15:12
  • What platform[s] are you targeting? The code you posted is not particularly portable or fast (though it is likely faster than a naive floating point implementation on some platforms), but the optimal solution depends heavily on both the hardware and OS ABI conventions in use. – Stephen Canon Mar 24 '10 at 22:35
  • @Stephen, My game is mainly for x86 and x86_64, various platforms (GNU, Windows, *BSD). I profiled this code on my machine, and it ran more slowly than the built-in fabs() routine. – Patrick Niedzielski Mar 27 '10 at 12:51

2 Answers2

10

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.]

Steve Jessop
  • 273,490
  • 39
  • 460
  • 699
  • I am assuming IEEE representation, that isn't a major problem. Other than that, thanks. I'll see if there is any more input, but if not, thanks! – Patrick Niedzielski Mar 24 '10 at 14:45
  • "float and int might not have the same endianness": on the same system? Can you give an example? – Rick Regan Mar 24 '10 at 17:38
  • @Rick: Is ARM soft float little-endian or natural-endian? I can't remember. Anyway the question says that the code can easily be extended to double, and the ARM FPA unit's doubles are neither little-endian *nor* big-endian, so questioner would definitely be out of luck if using an ABI with that double format. The point is just that the standard doesn't forbid it, this issue is tiny compared with the other portability considerations. – Steve Jessop Mar 24 '10 at 19:30
  • So ARM doubles are mixed endian ("nested" endian, if you will)? Yup, that would mess things up. Thanks for the example! – Rick Regan Mar 25 '10 at 13:38
  • @Rick: They can be. ARM has several ABIs corresponding to different floating point hardware. Originally there was no hardware, it was software-only. Then FPA was around for a while with its funny endianness. VFP is a newer floating-point unit which I think is "proper" natural-endian. – Steve Jessop Mar 25 '10 at 14:20
4

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.
Skizz
  • 69,698
  • 10
  • 71
  • 108
  • And don't forget, compilers are rubbish at optimising floating point code. It's not hard to improve on the compiler's FP code and that will have a bigger impact on performance than doing bit-fiddling. – Skizz Mar 24 '10 at 15:25
  • Alright, thank you. This seems a bit more elegant than the solution I found online. Using a #define wrapper should help reduce any cross platform problems. – Patrick Niedzielski Mar 24 '10 at 15:26
  • Not all Intel systems use the 80 bit x87 unit for floating-point code. On OS X, for example, floating-point is compiled to use the SSE unit by default (except for the `long double` type, which is codegen'd to x87 instructions); on a system that doesn't use the x87 unit for argument passing or returning results, a solution like this would impose considerable overhead vs. a solution that uses the core registers or SSE. – Stephen Canon Mar 24 '10 at 22:39
  • @Stephen: At first, I thought "Really?", but thinking about it, the SSE isn't stack based so the compiler has a much easier job of allocating registers, i.e. they stay put as opposed to the FPU where their position changes, e.g. FLDZ makes ST(0)->ST(1), ST(1)->ST(2) and so on. On the downside, the instruction set is different on the SSE, it's geared towards SIMD. Also, since Apple control the hardware so tightly, there's never a case where a CPU doesn't have SSE. – Skizz Mar 25 '10 at 10:36
  • @Skizz: Exactly. All Intel Macs are guaranteed to have SSE and SSE2, so the compiler can use it by default. In addition to the advantages you listed, SSE is quite a bit faster than x87. – Stephen Canon Mar 25 '10 at 14:36
  • @Stephen: Out of interest, do the Apple compilers have an option to use the FPU by default? – Skizz Mar 25 '10 at 14:59
  • @Skizz: 32 bit executables can be built with `-mno-sse` to codegen on the x87 unit. One has to be careful with 64 bit executables, because the 64-bit ABI requires that floating-point values are passed and returned in SSE registers. – Stephen Canon Mar 25 '10 at 16:27