Representing float point in hex with printf("%a", 3.14)

Viewed 197

A simple code like this printf("hex representation for %f is [%a]\n", 3.14, 3.14);, I expect the result to be 0x4048f5c3 (full binary version is: 0100 0000 0100 1000 1111 0101 1100 0011, 0x4.8f5c3p-1) according to reference websites below, but the compiled executable shows [0xc.8f5c3p-2] (binary: 1100.1000 1111 0101 1100 0011), why did the C compiler shows exponent as -2 instead of -1?

The compiler setting is:

"command": "c:/msys64/mingw64/bin/gcc.exe",
"args": [
            "-g",               
            "${file}",
            "-o",
            "${fileDirname}\\${fileBasenameNoExtension}.exe",
            "-Wall",
            "-Werror",
            "-std=c11"

Reference: https://users.cs.fiu.edu/~downeyt/cop2400/float.htm https://gregstoll.com/~gregstoll/floattohex/

2 Answers

The website you are using is displaying the binary representation of a floating-point number. For example, 3.14 is 40 48 F5 C3 in big-endian, or C3 F5 48 40 in little-endian.

The hex representation in C is the actual floating-point number in base 16, not it's binary representation. 0xc.8f5c3p-2 means c.8f5c3 * 2^(-2). If we convert this to decimal using the usual conversion algorithm, we get 3.14:

12(C) * 16^0 + 8 * 16^(-1) + 15(F) * 16^(-2) + 5 * 16^(-3) + 12(C) * 16^(-4) + 3 * 16^(-5) = 12 + 0.5 + 0.05859 + ... = 12.56 (with approximation)

Now multiply by 2^(-2), or divide by 4, and we get 3.14.

The difference between the 2 representations is shown here (note that this program technically causes undefined behavior in C, because of the uint32_t* to float* cast followed by dereference, but in this case the behavior is to use the binary representation to create a floating-point number, as one would expect):

#include <stdio.h>
#include <inttypes.h>

// Assuming IEEE 754 representation of 32-bit floats

int main(void)
{
    float x = 0xC.8F5C3p-2;
    uint32_t y = 0x4048F5C3;
    printf("Base-16 representation of %f is: %A\n", x, x);
    printf("Binary representation of %f is: 0X%"PRIX32"\n", *(float*)&y, y);
}

And this is the output I get on my computer:

Base-16 representation of 3.140000 is: 0XC.8F5C3P-2
Binary representation of 3.140000 is: 0X4048F5C3

Why did the C compiler show exponent as -2 instead of -1?

The issue here is that floating-point representations — whether using binary, decimal, or hexadecimal — tend not to be unique. Looking at your number in base 10, its scientific notation representation might be

    3.14 × 100

or

    0.314 × 101

or maybe even

    31.4 × 10-1

or

    0.0314 × 102

To solve the uniqueness problem we usually define a "normalized form" — but there can be multiple ways to make that definition! For example, we could say that there should always be exactly one digit to the left of the decimal point (3.14 × 100) ― or we could say that there should be 0 to the left of the decimal point but a nonzero digit immediately to the right (0.314 × 101). And there are other choices of normalization rule as well.

And then when it comes to printf %a it gets even more confusing, because the significand digits are hexadecimal but the exponent is a power of two. So even if we say we want "one digit" to the left of the radix point, we've got four different choices of what that digit could be, because we can effectively put the radix point between any of the bits of a hexadecimal digit!

We can illustrate this with your example of 3.14. In binary, rounded to 24 significant bits (which is IEEE single precision, a.k.a. float), it is

    0b1.10010001111010111000011 × 21

If we convert that directly to hexadecimal, we get

    0x1.91eb86 × 21

But we can shift that significand 1, 2, or 3 bits to the left, and still have just one hexadecimal digit to the left of the radix point:

    0x3.23d70c × 20
    0x6.47ae18 × 2-1
    0xc.8f5c30 × 2-2

And, in fact, on my computer, %a prints 3.14f as 0x1.91eb86p+1. But you said yours printed 0xc.8f5c3p-2 (and so did @DarkAtom's). But as we've just seen, both representations are equivalent.

As the other answer and comments have explained, the hexadecimal number you thought you might see, 0x4048f5c3, is not directly related to the value; it's the hexadecimal representation of the IEEE-754 single-precision raw encoding. Buried inside that encoding are a sign bit of 0, a biased exponent of 0x80, and a significand of, depending on how you look at it, either 0x91eb86 or 0x48f5c3. But now we can pretty easily see how those fit together, because the significand matches the hexadecimal patterns we've seen, and the biased exponent value 0x80 works out to an actual exponent of 128 - 127 = 1. (I said that the encoded significand was "depending on how you look at it, either 0x91eb86 or 0x48f5c3", but you can take my word for it that it all works out corresponding to 0x1.91eb86 × 2¹, where the leading 1 is implicit.)

I can't explain where that 0x4.8f5c3p-1 you mentioned came from, though.

Related