Closed Finii closed 8 months ago
If I remember correctly other projects have for that exact reason their own limited pow()
-like function that is exact (not even off by 1 ULP), for example Ryu and the more modern variants. We could in principle also use that approach, but that would make it all more complicated. I guess we do not want that kind of tables.
Configure time decisions and header only files do not mix (so easily). Took me some time to realize the problem.
Tried to address nitpicks.
Start from scratch, force push. Updated the description in the top of this page.
Tested on Ubuntu, Alpine, MacOs (M2, x86), Windows
We could make pow10
part of GUL.
And maybe error_in_ulp()
.
Both in a later PR.
Invite to discuss @alt-graph
[why] For
to_number<long double>()
to work we need astd::powl()
that is accurate - its errors directly influence our conversion output, usually amplified. In the past we just knew about Apple clang that had a problem withstd::powl()
being off a bit, but now also Alpine has the same problem via musl that it uses.[how] On parsing a string a possible number is expressed with scientific notification, for example
123.4E5
which means 123.4 * (10^5).The mantissa is converted to a normalized value as floating point number, (1.234 in the example) and the exponent is corrected for the shift of decimal places (5 becomes 7). So the example is transformed to 1.234 * (10^7).
To calculate the exponent the call
pow(10, 7)
is used. Any inaccuracy in the calculation directly influences the result. For very big (or small) exponents musl is off by more than 1000 ULPs.But in fact a much simpler function than
pow()
would be sufficient. The base is always 10 and the exponent is always integer. Usuallypow()
calculates the result withlog2()
andexp()
under the hood. But given the constraints we can get away with a much simpler approach.The new function
pow10(integer)
uses a table of precalculated results for the exponents -16 to 16. Values for the other (bigger/smaller) exponents are calculated with the right to left binary exponentiation. The table needed for the binary approach is reasonably small while giving good enough accuracy. Despite the fact that several multiplications are chained the result is usually still within 1 ULP to the correct result, only exponents that have almost all bits set (i.e. need a lot of multiplications (7 or more)) have an error of 2 ULP.As we now have an exact enough
pow()
substitude the platform specific exceptions and fallback tostd::strtold()
(that needs allocations) can be removed.The code assumes that
If long double has exponents with not exactly 11 or 15 bits the code and/or tests can break (fail).
There is no additional correction step to get the error in ULP down; but we never guaranteed a better conversion than 3 ULP off. That still holds. With one test we need to allow 1 ULP error where before it was 0 ULPs.
(*) The assumption is that floating point literals are handled correctly; rounded to the nearest representation = 0 ULP off of the ideal result.
[note] Here some examples of reported std::powl() accuracy:
Ubuntu 23.10 (GLIBC 2.38)
Alipne 3.19.1 (musl 1.2.4)
Sonoma 14.3 on M2 (libSystem 1336.61.1)
On Sonoma it seems double == long double.
Github MacOS CI machine (x86?) (libSystem 1319.0.0)
[note] Some useful links:
About floating point representations: https://en.wikipedia.org/wiki/Double-precision_floating-point_format https://en.wikipedia.org/wiki/Extended_precision
David M. Gay's strtod() (Expat) for example here: https://portal.ampl.com/~dmg/netlib/fp/dtoa.c
Rick Regan's Exploring Binary blog, for example https://www.exploringbinary.com/fast-path-decimal-to-floating-point-conversion/ https://www.exploringbinary.com/strtod-initial-decimal-to-floating-point-approximation/ https://www.exploringbinary.com/why-powers-of-ten-up-to-10-to-the-22-are-exact-as-doubles/
Fixes: #69