Follow @Openwall on Twitter for new release announcements and other news
[<prev] [next>] [<thread-prev] [thread-next>] [day] [month] [year] [list]
Date: Thu, 11 Apr 2024 22:34:41 -0400
From: Rich Felker <dalias@...c.org>
To: Peter Ammon <corydoras@...iculousfish.com>
Cc: musl@...ts.openwall.com
Subject: Re: [PATCH] Fix printf hex float formatting with precision

On Thu, Apr 11, 2024 at 06:17:25PM -0700, Peter Ammon wrote:
> printf hex formatting with precision may emit excess digits on
> targets where long double is an alias of double. For example, on
> ARMv7, the expression `printf("%.12a", M_PI)` outputs 13 digits past
> the decimal, instead of 12.
> 
> I believe the cause is a bogus rounding calculation in fmt_fp:
> 
> if (p<0 || p>=LDBL_MANT_DIG/4-1) re=0;
> else re=LDBL_MANT_DIG/4-1-p;
> if (re) {
>     round *= 1<<(LDBL_MANT_DIG%4);
>     while (re--) round*=16;
>     ...
> 
> I wasn't able to justify the calculation of `re`; I think it suffers
> from trying to round in terms of number of hex digits, which is
> tricky because the number of fractional mantissa bits may not be a
> multiple of 4.
> 
> I propose to fix it by working in bits instead of hex digits, as follows:
> 
> if (p>=0 && p<(LDBL_MANT_DIG-1+3)/4) {
>     int re = LDBL_MANT_DIG-1-(p*4);
>     long double round = 1ULL<<re;
> ....
> 
> This is justified as follows:
> 
> - The value to format has been scaled to the range [1, 2), so there
> is exactly one bit before the radix. Thus, the fractional portion of
> the mantissa may be printed at full precision with LDBL_MANT_DIG-1,
> divided by 4, rounding up; thus `(LDBL_MANT_DIG-1+3)/4`
> - The caller has requested a precision of p hex digits, so p*4 bits.
> - `re` is then the number of bits to round off, as LDBL_MANT_DIG-1-(p*4)
> - Thus we compute `round` as 2**re. Adding `round` will lose `re`
> bits of precision, rounding per the fp mode; subtracting this again
> will round the original value.
> 
> I also removed an initial factor of 8 from the `round` as it seemed
> incorrect to me; for example we may want to round only the last bit,
> in which case the min round of 8 would be too big.
> 
> I am by no means a numerics expert, so I very much welcome another
> pair of eyes. I will be happy to contribute a test.

Aside from one minor fix, this code dates back to the initial check-in
of musl, and based on the behavior and the initial value 8 for round,
I'm pretty sure it was written assuming we want the digit before the
radix point to be in the range [8,F] rather than 1, to pack the max
amount of precision into the output. For some reason, I decided this
was not the best behavior, but must have failed to compensate for that
in the rounding logic. There may be pre-0.5.0 trees somewhere I could
excavate to figure out exactly what happened, but that's my best
guess.

FWIW the bug seems to be fixed by making it do y*=8; e2-=3;

Offhand without delving deep into it, your fix sounds correct. It
could probably be factored as two parts, first just fixing the bug
(I think the only actual bug is the 8), and a second switching to your
better algorithm for computing the rounding term.

Rich

Powered by blists - more mailing lists

Confused about mailing lists and their use? Read about mailing lists on Wikipedia and check out these guidelines on proper formatting of your messages.