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