ItsASecret
ItsASecret

Reputation: 2649

C printf %a and %La

I have to reimplement printf(3) with C wihtout using any function that would do the conversion for me.

I thought I was done after I understood thanks you to guys how %a worked: How %a conversion work in printf statement?

Then I realized I didn't understand how the rounding was done so I asked: C printf float rounding and then thought that I was done after you helped me.

And now that %a is completely working just like the official one I thought %La would do exactly the same as %a but with a long double since the man only says:

Modifier a, A, e, E, f, F, g, G

l (ell) double (ignored, same behavior as without it)
L long double

And I discover that it outputs something completely different :'(

double a_double = 0.0001;
long double a_long_double = 0.0001;
printf("%a\n", a_double); #=> 0x1.a36e2eb1c432dp-14
printf("%La\n", a_long_double); #=> 0xd.1b71758e21968p-17

The %a result always begins with 1. and now I don't understand at all what the %La is doing.

Could you help me understand the process that transforms 0.0001 to 0xd.1b71758e21968p-17 ?

EDIT: the part that I really don't understand is why %a always outputs something that starts with 1. and not %La ?

EDIT2: To be even more precise: why does %a chooses to output 1. ...p-14 and %La chooses to output d. ...p-17 ?

Upvotes: 1

Views: 1500

Answers (3)

Chris Dodd
Chris Dodd

Reputation: 126328

You don't say what kind of machine you are working on here, but from the fact that you get a 0xd.... result from %La implies that this is a machine where long double is an unnormalized floating poing type (such as 80-bit floats on an 8087). With a normalized float, you will always get a result that starts with 0x1.... as you were guessing, but for unnormalized floats, there are mulitple representations of the same number, which is what you are seeing here.

The whole point of the %a conversion spec is to allow for printing a binary floating point number as text in a way that it can later be read back with scanf that will result in a bit-identical value on a machine with the same floating point representation.

Upvotes: 1

AntoineL
AntoineL

Reputation: 946

Any (finite, non-zero) floating point number has four distinct (and valid) hexadecimal floating-point representations:

  1. one which highest hexadecimal digit is between 8 and F
  2. another which highest hexadecimal digit is between 4 and 7
  3. another which highest hexadecimal digit is 2 or 3
  4. another which highest hexadecimal digit is 1

and they have successive increasing exponent values. You can pass from one to another using right shifting; this process might raise another digit to the right; there is no restriction in the C Standard about which representation you should use for the %a conversion.

With IEEE float (24-bit significand) or the usual double extended (64-bit significand, as with long double on i386 Linux), since the number of bits is divisible by 4, it is customary to use the first form for a normalized number, since it is the form which uses the fewest hexadecimal digits to fully represent the number (respectively 5 and 15 digits after the hexadecimal point .).

With IEEE double (53-bit significand), on the other hand, it looks better to use the fourth form and 13 hexadecimal digits after the point .: thus all the shown digits are actually representing data. The same would happen with IEEE quad (formally binary128, 113-bit significand), with 28 hexadecimal digits after the point.

These representations have also the nice property to match the in-memory representation of the numbers (this is why implementations are guided to do so by a footnote in the C99 standard.)

If we now look at your question, the first example (double) matches the guidelines above, having 1 as first digit and then 13 hexadecimal digits after the point.

The second example is more crafty. First a double constant is stored into a long double variable: depending on the compiler, the constant may be rounded to double's precision (as it seems to be done here.) This means that the bits after the 53th will be all zeroes. Then the %La conversion used, also according to the guidelines above, thus choosing the first representation, start with a D (1101xxx in binary, to be compared with the 1.A which translated to 11010xxx); here too there are only 13 hexadecimal digits after the point since the compiler discarded printing the unnecessary zeroes (due to the rounding.)

Note that you cannot use printf's %a conversion for float.

Upvotes: 8

R.. GitHub STOP HELPING ICE
R.. GitHub STOP HELPING ICE

Reputation: 215317

It's not clear what aspect of the result it surprising you. If it's the difference in exponents and digits, that's just because of poor normalization. Note that for the first result you get a 1 before the radix point and for the second you get a d before it. Shift d right 3 bits and you have a 1, and the lower bits (101) shift into the next position after the radix point, giving a, as expected.

As for how 0.0001 is transformed to 0xd.1b71758e21968p-17, it's just a matter of finding the binary floating point value that's closest to the decimal number 0.0001. The mechanism for how that's done efficiently is somewhat involved, but the concept is simple.

Upvotes: 1

Related Questions