Closed
Description
Bug report
Bug description:
>>> import math
>>> math.ldexp(6993274598585239, -1126)
5e-324
>>>
The correct result would be 1e-323. This is obviously a bug in the Windows ldexp implementation (it works fine on Linux). But it would be good, if it could be fixed on the CPython side with a workaround.
math.ldexp is used by mpmath to round from multiprecision to float, leading to incorrect rounding on Windows.
CPython versions tested on:
3.13
Operating systems tested on:
Windows
Activity
skirpichev commentedon Apr 24, 2025
For a context: mpmath/mpmath#942
mmmp-oo commentedon Apr 24, 2025
Hi! 👋
This issue appears to be caused by platform-specific differences in floating-point handling, particularly on Windows. For consistency across platforms, a workaround is to use arbitrary-precision libraries such as
mpmath
:Of course, since
math.ldexp
is a native function, a permanent solution would need to be implemented within CPython itself.Hope this helps! 😊
skirpichev commentedon Apr 24, 2025
Just a bug in the platform libc. @tkreuzer, could you please describe your system (OS, hardware)?
It's not a real workaround, because OP want native CPython floats, not mpmath's numbers. In fact, math.ldexp() is used in the mpmath to provide the
__float__
dunder for mpf type.It's possible to use gmpy2:
Though, it's not an option for the mpmath itself.
tkreuzer commentedon Apr 24, 2025
Windows 10 on an Intel i/-1065G7.
I implemented a workaround myself now.
skirpichev commentedon Apr 24, 2025
On mpmath's master:
With your patch (I doubt it's correct):
tkreuzer commentedon Apr 24, 2025
It's slower, I get it. Would not be an issue, if it was implemented in C 🙂
Alternatively you can probably use ldexp for anything that is not a denormal (would need to test that). Then you can simplify the code a bit. And you can replace the shift loop with something like
skirpichev commentedon Apr 25, 2025
python -VV
output might be helpful.From quoted issue: Python 3.10.11 (tags/v3.10.11:7d4cc5a, Apr 5 2023, 00:38:17) [MSC v.1929 64 bit (AMD64)] on win32
I would like to see if someone else can reproduce this on same platform.
This can be mitigated coding in C, but that's not for the mpmath.
It's also incorrect. Besides raising OverflowError in an appropriate place, you probably should remove
if sign == 1
branch inmpf_to_float()
.FYI: mpmath/mpmath#945
You meant "for anything, that not produces a denormal"? Because
6993274598585239.0
is a normal float. I doubt it's cheap.CC @tim-one
Workaround buggy lgexp() on win32
tim-one commentedon Apr 25, 2025
The Windows ldexp doesn't round - it chops (truncates: "round to 0"). Whether that's "a bug" is arguable, but Microsoft hasn't budged on this at all.
To make this easier to follow, here's the simplest "failing" case:
So long as the result isn't subnormal, no problem. The original intent of ldexp was to give a fast way to change the exponent without changing the mantissa bits at all. Subnormals don't play nice with that view, though.
Python's math module generally intends to reproduce the platform's C library results for functions of the same name.
There's no clear win to be had here. It's impossible for ldexp to both round and to match C programmers' expectations of how ldexp works on Windows.
If you want the same semantics as multiplying by a power of 2, multiply by a power of 2. The days when ldexp was faster than float multiplication are now ancient history.
Not that it's trivial. In the above, e.g., 2.0**1073 isn't representable as an IEEE double. In bad cases it can take two stages: first use ldexp to shift the input to the smallest normal binade. No information is lost. Then multiply by the appropriate negative power of 2 to finish the job. Let the hardware take care of rounding. There's no need to pick apart - or do arithmetic on - the bits yourself.
skirpichev commentedon Apr 25, 2025
Ah, that explains things for me. Does it happens on all M$ systems or rather limited (e.g. to win32)? (Disclaimer: I had windows box nearby 10+ years ago.)
Sorry, but it seems to be a bug. Standard says: "If a range error due to underflow occurs, the correct result (after rounding) is returned." Rounding mode could be ignored only if no range error occurs.
Are you argue to get rid of CPython's versions of tgamma()/lgamma() and switch to libm's versions? ;-)
So, sometimes we have workarounds for buggy libm functions. This might be the case.
tim-one commentedon Apr 25, 2025
I'm not a Windows expert. It's something I've learned from the systems I personally use. It was true under DOS from the start, and is still so under my current 64-bit Windows 10 Pro.
The great thing about standards is that there are so many of them 😉. You need to identify the exact standard you have in mind. The accepted answer here:
lets Microsoft off the hook because their C implementation may not define
__STDC_IEC_559__
as 1. Well, they didn't know for sure whether it did, but the accepted answer here says__STDC_IEC_559__
is set to 0 on Windows by both gcc and clang (but to 1 on Linux)Which isn't the final word either. Hence it's "arguable", but it's not an argument I want to engage in.
If platform libms have since improved to be better than horrid on those functions, yes, it would be best if we did lose ours. You already recently discovered that, on the platform you used, its gamma is better than ours, We're not doing users on that platform much good by hiding their platform's better implementation. But since we took them over, we have our own backward compatibility worries now.
Microsoft has decades of backward compatibility to worry about with
ldexp()
, which is why I expect - but don't know - they never change it.It might be. But if we do take it over on Windows, our docs need to be clear that Python's
ldexp
differs in this respect from the native Windows version. The docs should have done so for the gamma functions when we took them over.It's a fight I don't want to take part in. But I'd be +1 on leaving
ldexp()
alone, and introducing a new, say,ldexpr()
("ldexp rounded") function that promises to round on all platforms. It would the same as the platformldexp
on most popular platforms.When I care, I multiply by a negative power of 2. It's cheap to compute 2**-n via ldexp(1.0, -n). But n has to be <= 1074, else that underflows to 0.
So that's the core "trick" to implementing a rounded variant of ldexp on Windows without tedious, slow, and error-prone bit fiddling.
frexp()
can be used to get the input's exponent. There's no need to look at (let alone change) the mantissa bits.tim-one commentedon Apr 25, 2025
Here's a very lightly tested workaround for Windows, which defers to the platform function unless rounding may be needed. Rounding (if any is needed) is done by the multiply at the end.
Details
Note that it's important to do no more than one multiply. lest subtle "double rounding" bugs be introduced. The intent is that no information of any kind is lost before the single multiply.
48 remaining items