2

When representing two float32's as int32 or uint32 (using union or casting pointer types), Within a limited range (0..1), can these values be compared against each other, that would match the result of the comparison when executed as floats?

Example:

int float_as_int(float f) {
    union { int i; float f; } u;
    u.f = f;
    return u.i;
}

/* Assert the following is always true for values in a limited range. */
void assert_compare_works_as_expected(float a, float b) {
    assert(float_as_int(a) < float_as_int(b) == a < b);
}

/* Psudocode */
int main() {
    for f in 0..1 {
        assert_compare_works_as_expected(f_prev, f);
        f_prev = f;
    }
}

If not, what float ranges map directly to ints so comparison can be done?

ideasman42
  • 42,413
  • 44
  • 197
  • 320
  • 2
    Float accepts values up to `3.402823 × 10^38` while `uint32_t` up to `(2^32)-1`. You are safe until your float is not bigger than maximal value of int you are casting to. – unalignedmemoryaccess Jul 19 '17 at 08:27
  • 2
    A `float` in the range `INT_MIN` to `INT_MAX`. – Weather Vane Jul 19 '17 at 08:29
  • C++ may be a bit easier to work with in this instance because of `numeric_limits`. I suppose the same could be done in C using the defines. Also see [Normalized Integer to/from Float Conversion](https://stackoverflow.com/q/37580501/608639) and [Convert/Quantize Float Range to Integer Range](https://stackoverflow.com/q/599976/608639) – jww Jul 19 '17 at 08:32
  • 1
    According to your modified question, answer is no. They can't be compared as float structure is completely different to int structure. – unalignedmemoryaccess Jul 19 '17 at 08:33
  • There is no requirement in C as to how `float` should be represented. – Weather Vane Jul 19 '17 at 08:35
  • 1
    @WeatherVane that's true, but since most of them have IEEE 754 standard implemented, we can say that answer is still no. – unalignedmemoryaccess Jul 19 '17 at 08:37
  • 1
    @tilz0R If we assume IEEE 754 standard for representing floats then the comparison should be valid directly. Because exponents are stored before the mantissa. – Ajay Brahmakshatriya Jul 19 '17 at 08:46
  • @AjayBrahmakshatriya For example, number `4` in `int` is represented as `0x00000004` while the same number `4` is represented in float (by IEEE754 standard) as `0x40800000`. How is this the same? – unalignedmemoryaccess Jul 19 '17 at 08:50
  • @tilz0R Yes, 4 is 0x40800000 in float but 5.0 is 0x40a00000. The comparison `0x40800000 < 0x40a00000 -> 4.0 < 5.0` – Ajay Brahmakshatriya Jul 19 '17 at 08:54
  • 2
    @tilz0R OP is not comparing a float with other unsigned. I believe he is type punning 2 floats into unsigned integers and comparing the integers. – Ajay Brahmakshatriya Jul 19 '17 at 08:54
  • **You cannot cast the pointer**, it is UB, and it will bite you. The compilers will keep the floats in floating point registers, they're not flushed into memory upon access to some int pointer. – Antti Haapala -- Слава Україні Jul 19 '17 at 10:16
  • @WeatherVane [Almost completely right](https://stackoverflow.com/users/4142924/weather-vane) A float in the range `[INT_MIN.99999.... to INT_MAX.99999....]` as conversion from `float` to `int` is defined for a slightly larger range. – chux - Reinstate Monica Jul 19 '17 at 11:41
  • 1
    @chux agree its not great, I didn't think the question would be misunderstood as it was, I've since added an example in the question. – ideasman42 Jul 19 '17 at 13:17

7 Answers7

2

The long answer is: Yes, but with multiple restrictions and caveats. Comparing floats in the range 0 to 1 as integers may yield the correct result in many cases. As a matter of fact, the values need not be restricted to this range:

  • Assuming the float type has 32-bit single precision IEEE-754 representation and the int type has 32-bit 2's complement representation without padding bits (ie int isint32_t`);

  • Assuming neither of the float values are NaN;

  • Assuming the values are in normal or sub-normal form;

  • Assuming float and int representations have the same endianness in memory;

  • if the values are equal, comparing as int will yield the correct result unless you are comparing +0.0 and -0.0.

  • if the values are not both negative, the comparison as int may indeed yield the correct result, but the C Standard makes no guarantee and accessing the float values as int breaks the strict aliasing rule in most circumstances.

  • if the values are both negative and the other constraints are matched, the comparison result should be negated.

For your side question: some integer values may indeed have an identical representation as int and float, notably 0, but the floating point value -0.0 has a different representation yet compares equal to 0.0 as float. There are only two other special cases of integers that have the same representation as int32_t and IEEE-754 float: 1318926965 (0x4E9D3A75) and -834214802 (0xCE46E46E).

You can look at the actual representation of floating point values for the IEEE format, overwhelmingly common but not mandated by the C Standard here:

For your specific case, if you know the values are in the range 0 to 1, the only special case you should handle is the negative 0. You can for the sign as positive by masking the value and the code will look like:

/* return 0 if p1 and p2 point to the same float value,
   -1 if p1 points to a smaller value and
   +1 otherwise.
 */
int compare_floats_as_ints(const int32_t *p1, const int32_t *p2) {
    int32_t i1 = *p1 & 0x7fffffff;
    int32_t i2 = *p2 & 0x7fffffff;
    return (i1 > i2) - (i1 < i2);
}
chqrlie
  • 131,814
  • 10
  • 121
  • 189
  • If the exponent is stored before the mantissa (and a single cohurt is enforced), the constraint *if the values are within the same range of powers of 2* can be relaxed right? – Ajay Brahmakshatriya Jul 19 '17 at 08:50
  • @AjayBrahmakshatriya: if the values are not both negative, it seems the comparison as int can work for some architectures. Interesting. – chqrlie Jul 19 '17 at 09:23
  • Note that this question is **specifically** about values from 0 to 1. But this answer is phrased as if the question is about all floating point values (which won't work of course). To the question I asked, the short answer is **Yes** AFAICS, note that while I did edit the question heavily, the 0..1 part was there from the start. – ideasman42 Jul 19 '17 at 13:23
  • @ideasman42: indeed the answer is yes, if a number of conditions are met and the values are not `+0.0` and `-0.0`. – chqrlie Jul 19 '17 at 15:10
2

The way I interpret you question: When interpreting the bit pattern of a floating point number in memory as a int, will the usual comparison operators behave the same for floating point numbers and integers.

The answer is no. Strictly speaking, the C standard has a few possible integer representations and even more possible float representations. But even limiting ourselves to what pretty much everyone does - two's complement integers and ieee754 binary32 floating point, the answer is still no.

The simplest example: floating points have two zeroes. The bit pattern 0x00000000 and the bit pattern 0x80000000 represent the numbers 0 and -0. They both compare equal as floating point, they wouldn't if interpreted as integers. Then you have two infinities, and a whole lot of bit patterns that represent NaN.

You could enumerate the special cases and avoid them and then I'm pretty sure that a comparison as integers would work, but whatever performance gain you get from integer comparisons (which I assume is the point here) you'd gain you'd lose much more in avoiding the special cases.

Responding to your last edit: Yes, in the range 0 to 1, the bit pattern for ieee754 floating point numbers is such that when interpreted as integers the normal integer comparison operators will behave the way you want. It's a bad idea, but it would work.

Art
  • 19,807
  • 1
  • 34
  • 60
1

The answer is yes, with the qualifications that:

  • float & int are the same size.
  • inputs are positive, float values that don't include:
    -0.0, -inf, nan, -nan.

Details:

  • Both signed int and unsigned int can be used with matching comparisons.
  • Assumes IEEE754 32bit floating point.
  • Both int & float need to be the same endian (do architectures exist where this is not the case?)
  • Not just 0-1, 0.0..FLT_MAX and inf can be compared.
  • For completeness, negative values: -0.0..-FLT_MAX can be compared too but will always have a flipped order, eg:
    assert(float_as_int(a) > float_as_int(b) == a < b);

In this case, 2x floats represented as ints can be compared, giving the same results.

Here is a evidence that this can work:

C code, tests the full, unsigned float range.

#include <math.h>
#include <float.h>
#include <stdio.h>

int main() {
    unsigned step = 0;
    union {
        float f;
        unsigned u;
    } value;

    value.f = 0.0;

    unsigned u_prev = value.u;

    while (value.f != FLT_MAX) {
        value.f = nextafterf(value.f, FLT_MAX);
        unsigned u = value.u;
        if (u <= u_prev) {
            printf("At value %f, step %u, comparisons don't match\n", value.f, step);
            break;
        }
        u_prev = u;
        step++;
    }
    printf("Tested %u times\n", step);

    return 0;
}

a Python3 script, checking all values from 0-1 with nextafterf, note that this is slow (hence the C version above).

def main():
    from struct import pack, unpack
    as_u32_prev = None
    value = 0.0
    while True:
        # transmute float to int
        as_u32 = unpack('I', pack('f', value))
        if as_u32_prev is not None:
            if as_u32_prev > as_u32:
                raise Exception("f32/u32 comparisons don't match")
        as_u32_prev = as_u32
        if value == 1.0:
            break
        value = nextafterf(value, 1.0)

# Boiler Plate, see: https://stackoverflow.com/questions/6063755

import ctypes
import sys
from sys import platform as _platform

if _platform == "linux" or _platform == "linux2":
    _libm = ctypes.cdll.LoadLibrary('libm.so.6')
    _funcname_f = 'nextafterf'
elif _platform == "darwin":
    _libm = ctypes.cdll.LoadLibrary('libSystem.dylib')
    _funcname_f = 'nextafterf'
elif _platform == "win32":
    _libm = ctypes.cdll.LoadLibrary('msvcrt.dll')
    _funcname_f = '_nextafterf'
else:
    # these are the ones I have access to...
    # fill in library and function name for your system math dll
    print("Platform", repr(_platform), "is not supported")
    sys.exit(0)

nextafterf = getattr(_libm, _funcname_f)
nextafterf.restype = ctypes.c_float
nextafterf.argtypes = [ctypes.c_float, ctypes.c_float]

main()

Note that other answers here say that this wont work in all cases, I'd be interested to know which cases the examples in this answer would fail, (besides architectures where float and int aren't 4 bytes in size).

ideasman42
  • 42,413
  • 44
  • 197
  • 320
  • For example, number `4` in `int` is represented as `0x00000004` while the same number `4` is represented in float (by IEEE754 standard) as `0x40800000`. How is this the same? – unalignedmemoryaccess Jul 19 '17 at 08:51
  • The values are not the same, but the comparison is (when both values are transmuted to ints and compared) - for values 0-1 at least. – ideasman42 Jul 19 '17 at 08:55
  • @chqrlie are you sure? - the C code above deals only with positive floats. – ideasman42 Jul 19 '17 at 09:34
  • "qualification that inputs are positive, normalized float values" should be "qualification that inputs are positive, float values". normal and sub-normals work – chux - Reinstate Monica Jul 19 '17 at 11:49
  • A key element missing is the _assumption_ that the endian of `float` and `int` are the same. There are not specified to be so. So this is a failing case. – chux - Reinstate Monica Jul 19 '17 at 11:57
  • Also missing: is the _assumption_ `float`, and `unsigned` are the same size and `unsigned` has no padding - a failing case. Part of this was implied in the question, yet "float32's as int32 or uint32" is open for interpretation as those are not standard types. Better to use `uint32_t` since you have all ready assumed IEEE `float`. – chux - Reinstate Monica Jul 19 '17 at 12:02
  • "Here is a proof that this works:" is not proof, but evidence that it can work. Other platforms could have failed given the above concerns. – chux - Reinstate Monica Jul 19 '17 at 12:02
  • @ideasman42: oops, on the contrary: since IEEE uses sign+magnitude, the values must **not** be both negative for the comparison as `int` to succeed. – chqrlie Jul 19 '17 at 12:31
  • @ideasman42 If you like, I can delete these comments as I have rolled them into an answer. – chux - Reinstate Monica Jul 19 '17 at 12:32
  • @chqrlie negatives work if `int` is signed magnitude too. Yes - only on a machine in a silicon graveyard. ;-) – chux - Reinstate Monica Jul 19 '17 at 12:38
1

Can 32 bit floats between zero and one be compared (with the same outcome) if they're transmuted to int/uint?

Using a union will work for comparing float in the range (zero and one) and if all the list below can be insured.

Otherwise the general answer is no. It might work, it might not.

union x {
  float f;
  int32_t i;
  uint32_t u;
} a,b ;
a.f = foo(); 
b.f = foo(); 
// for all a.f and b.f in range
assert((a.u > b.u) == (a.f > b.f));
assert((a.u < b.u) == (a.f < b.f));
assert((a.u == b.u) == (a.f == b.f));
// also for a.i
  1. float is binary32
  2. The integer type is not padded. (example: (u)intN_t) types.
  3. The integer may be any encoding: 2's complement, 1's complement, sign-magnitude
  4. The integer has same endian as float. C does not require this.
  5. The integer/FP type size are the same. (This was implied in the question).

This also works for the extended range of [+0.0 ... +INF]. It does not hold for -0.0.


To compare 2 any finite/infinite float given the same endian-ness, expected float encoding and the above conditions:

int32_t sequence_f(float x) {
  union {
    float f;
    int32_t i32;
    uint32_t u32;
  } u;
  assert(sizeof(float) == sizeof(uint32_t));
  u.f = x;
  if (u.i32 < 0) {
    u.u32 = 0x80000000 - u.u32;
  }
  //  printf("% 16.8e % 11d\n", x, u.i32);
  return u.i32;
}

// return + +,0,- without FP math.
// valid for all finite/infinite values, not valid for NaN
// +0.0 -0.0 compare the same.
int compare(float fa, float fb) {
  int32_t ia = sequence_f(fa);
  int32_t ib = sequence_f(fb);
  return (ia > ib) - (ia < ib);
}

This is useful for embedded applications that need to perform FP compare, yet not other FP math.


In general, casting is UB due to anti-aliasing.

chux - Reinstate Monica
  • 143,097
  • 13
  • 135
  • 256
  • 1
    quite an elegant way to handle negative values including `-0.0`. – chqrlie Jul 19 '17 at 15:29
  • @chqrlie For C99 onward, yes type punning is OK [ref](https://stackoverflow.com/q/11639947/2410359) – chux - Reinstate Monica Jul 19 '17 at 16:47
  • `int32_t` is guaranteed to be 32-bit 2's complement with no padding, so you only need points 1 and 4 in your list – M.M Jul 27 '17 at 04:05
  • @M.M Agree - could minimize to those 2, yet `(u)int32_t` are example integer types. General types would need additional conditions, as you say, that are all ready satisfied with the classic fixed-width types and binary32. – chux - Reinstate Monica Jul 27 '17 at 05:12
1

The answer first

Yes, you can compare a binary representations of float as long as they are using IEEE-754 format, integers are 2's complement, you are comparing positive values or positive value with the negative, and you are treating the special cases properly.

Snippet on Ideone.com in C

The explanation

An IEEE-754 float (4 bytes) has the following structure:

     s::::::::.......................    meaning
    31                              0    bit #

    s = sign bit, : = exponent, . = mantissa

The value is coded as a mantissa * 2 ^ exponent. The exponent range is 0x00 to 0xFF, and it represents values -127 to 128 (0x7F corresponds to 0).

The mantissa represents a binary fraction with an implied 1b always present to the left of the decimal. Each bit of mantissa represents a value of 2 ^ -(n+1), where n is an index of a bit from the left. The leftmost bit of mantissa has a value of 0.5, second on the left 0.25, third on the left 0.125, and so on.

The value 0.5 will have mantissa b0000... (b1.0) and the exponent 0x7E (-1).
The value 0.03 will have mantissa b1110... (b1.1110...) and the exponent 0x79 (-6).
These values would be stored as:

0 01111110 00000000000000000000000    (0x3F000000)
0 01111001 11101011100001010001111    (0x3CF5C28F)

The lowest value you can store is 1.0 * 2 ^ -127 (all bits of mantissa and exponent are zero). That is a special case and represents a 0. The exponent value 0xFF is reserved for NaN and infinity.

Floating point number representation

You can compare a negative float value with the positive if you are using signed int because the sign flag is being stored in the highest bit for both signed int and float. You can not compare two negative float values like this tho, as the negation of int is not being done by inversion of the sign flag only.

0

As from my understanding, you want to read float number from memory as unsigned/signed integer and I will give you answer for that.

You cannot compare float with signed or unsigned int (32-bit).

For example, number 4 in int is represented as 0x00000004 while the same number 4 is represented in float (by IEEE754 standard) as 0x40800000.

unalignedmemoryaccess
  • 7,246
  • 2
  • 25
  • 40
-3

It does not matter if you compare uint from the union with float or the float itself. Same rules.

if you have two operations:

union {
  float fl;
  uint32_t ui;
} a, b

float a.fl = (math expressions);
float b.fl = (another math expressions); // both giving theoretically the same results

// comparition of

if(a.fl == b.fl) .... //UB - as float shall not be compared for being equal
if(a.ui == b.ui) .... //UB - as ui is an unsigned representation of the bytes of fl
0___________
  • 60,014
  • 4
  • 34
  • 74