Why do printf implementators prefer to print "%a" numbers with a leading 1?

Viewed 156

Recently I wrote the following program to test printing different floating point types with the %a specifier:

#include <stdio.h>
#include <stdlib.h>
#define ELEM(x)   { #x, x }
#define FLT_C(x)  ELEM(x##f)
#define DBL_C(x)  ELEM(x)
#define LDBL_C(x) ELEM(x##L)
#define TEST(TYPE, C, PRI) do { \
    /* some random numbers, do not matter really */ \
    const struct { const char *str; TYPE val; } nums[] = { \
        C(123.456), C(456.789), \
        C(123.678e10), C(678.890e-10), \
        C(1/0.3), C(0.5) \
    }; \
    /* foreach number in nums */ \
    for (size_t i = 0; i < sizeof(nums)/sizeof(*nums); ++i) { \
        printf("%15s %-20s = %"PRI"a\n", #TYPE, nums[i].str, nums[i].val); \
        /*                     ^^^^^^^ - print with %a with proper specifier */ \
    } \
} while(0)

int main() {
    printf("%38s\n", 
#if defined __GLIBC__
    "GLIBC"
#elif defined __NEWLIB__
    "NEWLIB"
#else
    "alpine"
#endif
    );
    TEST(float,       FLT_C,  "");
    TEST(double,      DBL_C, "l");
    TEST(long double, LDBL_C, "L");
}

I ran the program with different standard library implementations I had at hand (Godbolt link):

$ gcc -Wall -Wextra /tmp/1.c && ./a.out && arm-none-eabi-gcc --specs=rdimon.specs -u_printf_float -u_scanf_float -specs=nosys.specs /tmp/1.c && qemu-arm /tmp/a.out && docker run -ti --rm -v /tmp/1.c:/tmp/1.c:ro alpine sh -c 'apk add build-base >/dev/null && gcc /tmp/1.c && ./a.out'
                                 GLIBC
          float 123.456f             = 0x1.edd2f2p+6
          float 456.789f             = 0x1.c8c9fcp+8
          float 123.678e10f          = 0x1.1ff5d6p+40
          float 678.890e-10f         = 0x1.2394bep-24
          float 1/0.3f               = 0x1.aaaaaap+1
          float 0.5f                 = 0x1p-1
         double 123.456              = 0x1.edd2f1a9fbe77p+6
         double 456.789              = 0x1.c8c9fbe76c8b4p+8
         double 123.678e10           = 0x1.1ff5d523p+40
         double 678.890e-10          = 0x1.2394beb1a4116p-24
         double 1/0.3                = 0x1.aaaaaaaaaaaabp+1
         double 0.5                  = 0x1p-1
    long double 123.456L             = 0xf.6e978d4fdf3b646p+3
    long double 456.789L             = 0xe.464fdf3b645a1cbp+5
    long double 123.678e10L          = 0x8.ffaea918p+37
    long double 678.890e-10L         = 0x9.1ca5f58d208ac05p-27
    long double 1/0.3L               = 0xd.555555555555555p-2
    long double 0.5L                 = 0x8p-4
                                NEWLIB
          float 123.456f             = 0x1.edd2f2p+6
          float 456.789f             = 0x1.c8c9fcp+8
          float 123.678e10f          = 0x1.1ff5d6p+40
          float 678.890e-10f         = 0x1.2394bep-24
          float 1/0.3f               = 0x1.aaaaaap+1
          float 0.5f                 = 0x1p-1
         double 123.456              = 0x1.edd2f1a9fbe77p+6
         double 456.789              = 0x1.c8c9fbe76c8b4p+8
         double 123.678e10           = 0x1.1ff5d523p+40
         double 678.890e-10          = 0x1.2394beb1a4116p-24
         double 1/0.3                = 0x1.aaaaaaaaaaaabp+1
         double 0.5                  = 0x1p-1
    long double 123.456L             = 0x1.edd2f1a9fbe77p+6
    long double 456.789L             = 0x1.c8c9fbe76c8b4p+8
    long double 123.678e10L          = 0x1.1ff5d523p+40
    long double 678.890e-10L         = 0x1.2394beb1a4116p-24
    long double 1/0.3L               = 0x1.aaaaaaaaaaaabp+1
    long double 0.5L                 = 0x1p-1
                                alpine
          float 123.456f             = 0x1.edd2f2p+6
          float 456.789f             = 0x1.c8c9fcp+8
          float 123.678e10f          = 0x1.1ff5d6p+40
          float 678.890e-10f         = 0x1.2394bep-24
          float 1/0.3f               = 0x1.aaaaaap+1
          float 0.5f                 = 0x1p-1
         double 123.456              = 0x1.edd2f1a9fbe77p+6
         double 456.789              = 0x1.c8c9fbe76c8b4p+8
         double 123.678e10           = 0x1.1ff5d523p+40
         double 678.890e-10          = 0x1.2394beb1a4116p-24
         double 1/0.3                = 0x1.aaaaaaaaaaaabp+1
         double 0.5                  = 0x1p-1
    long double 123.456L             = 0x1.edd2f1a9fbe76c8cp+6
    long double 456.789L             = 0x1.c8c9fbe76c8b4396p+8
    long double 123.678e10L          = 0x1.1ff5d523p+40
    long double 678.890e-10L         = 0x1.2394beb1a411580ap-24
    long double 1/0.3L               = 0x1.aaaaaaaaaaaaaaaap+1
    long double 0.5L                 = 0x1p-1

I tested 3 implementations and all except glibc with long double print the hexadecimal floating point number with a leading 0x1... Why 0x1?

Is there a specific reason why 3 different implementations chose 0x1 as the initial part for printing a hexadecimal floating point number? Is it simplifying some things? Is this mandated by some standard? Why not always start with the initial bit set, i.e., with 0x8/0x9/.../0xf and adjust the exponent by -3? That way the representation will be shorter in some cases.

Source code research: glibc just prints '1' in printf_fphex#285, newlib just prints '1' in cfprintf#L1746 and musl does frexpl()*2 in fmt_fp#L210 before converting to digits. Is there any reason the initial 0x1 is so important that implementations specifically want to print an initial 1? (And on a side note, why does glibc with long double not do it?)

@edit Added 1/3.0L. And here's a small side-by-side comparison of long doubles:

v>      123.456L                 456.789L                 123.678e10L       678.890e-10L              1/0.3L                   0.5L
GLIBC   0xf.6e978d4fdf3b646p+3   0xe.464fdf3b645a1cbp+5   0x8.ffaea918p+37  0x9.1ca5f58d208ac05p-27   0xd.555555555555555p-2   0x8p-4
MUSL    0x1.edd2f1a9fbe76c8cp+6  0x1.c8c9fbe76c8b4396p+8  0x1.1ff5d523p+40  0x1.2394beb1a411580ap-24  0x1.aaaaaaaaaaaaaaaap+1  0x1p-1
NEWLIB  0x1.edd2f1a9fbe77p+6     0x1.c8c9fbe76c8b4p+8     0x1.1ff5d523p+40  0x1.2394beb1a4116p-24     0x1.aaaaaaaaaaaabp+1     0x1p-1
2 Answers

This most likely because most implementations use IEEE754 representation for floating point numbers.

This representation store a number of precision bits explicitly (23 for single, 52 for double), but also a single implicit bit which is always 1. So by always printing the value 1 to the left of the radix point, the values to the right correspond exactly to the precision part of the representation. This allows for easier debugging of values stored in this format.

Why printf implementators prefer to print %a number with leading 1?

First, all implementations meets the C spec*.

... A double argument representing a floating-point number is converted in the style [-]0xh.hhhhp+-d, where there is one hexadecimal digit (which is nonzero if the argument is a normalized floating-point number and is otherwise unspecified) before the decimal-point character and the number of hexadecimal digits after it is equal to the precision; if the precision is missing and FLT_RADIX is a power of 2, then the precision is sufficient for an exact representation of the value; ... C17 § 7.21.6.1 8


Using double constants obscures the test. Better to append a L to the constants: (123.456 --> 123.456L). As is, with long double nums[], all the values have trailing zeros precision bits when saved as long double and "%La" lops trailing significant '0' in printing.

Note in the long double 1.234560e+02 case, 0xf.6e978d4fdf3b8p+3 and 0x1.edd2f1a9fbe77p+6 are both 53 significant digits.

The rational I see implementations use 1 or 1 to F to accomplish 1 of 2 goals:

  • Always lead with 1 for a non-zero finite value. If the FP significant has other than 4*n+1 digits, the nth hex digit will not use all 16 choices. (I'll call this left justified.)

  • Lead with [1 to F or less] for a non-zero finite value such that a full precision output (e.g. 64 bits for select long double) could use all 16 choices in the least significant hex digit. (I'll call this right justified.)

In the case of GLIBC long double, (assuming 10-bit extended FP) had the value had some non-zero trailing precision bits (use L), I'd expect it would have printed 16 hex digits for the significant whereas the others would have used 17 hex digits.

I see a preference to the first one "use a 1" as it is simpler to explain: "Always lead with 1 for a non-zero finite value."


*, except maybe long double fails to print with sufficient precision in at least the NEWLIB case (per my testing using L). I'll research this more.

Related