From 812276cc5509fb3ac23f5544672d4b80765219ed Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Tue, 29 Apr 2025 11:44:39 +0300 Subject: [PATCH 1/9] gh-132876: workaround broken ldexp() on Windows 10 Co-authored-by: Tim Peters --- ...-04-29-11-48-46.gh-issue-132876.lyTQGZ.rst | 1 + Modules/mathmodule.c | 20 +++++++++++++++++++ 2 files changed, 21 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2025-04-29-11-48-46.gh-issue-132876.lyTQGZ.rst diff --git a/Misc/NEWS.d/next/Library/2025-04-29-11-48-46.gh-issue-132876.lyTQGZ.rst b/Misc/NEWS.d/next/Library/2025-04-29-11-48-46.gh-issue-132876.lyTQGZ.rst new file mode 100644 index 00000000000000..a28a6a474c39e8 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-04-29-11-48-46.gh-issue-132876.lyTQGZ.rst @@ -0,0 +1 @@ +Workaround broken ``ldexp()`` implementation on Windows 10. diff --git a/Modules/mathmodule.c b/Modules/mathmodule.c index 11d9b7418a25a2..0ef6290950de23 100644 --- a/Modules/mathmodule.c +++ b/Modules/mathmodule.c @@ -2163,6 +2163,26 @@ math_ldexp_impl(PyObject *module, double x, PyObject *i) } else { errno = 0; r = ldexp(x, (int)exp); +#if _MSC_VER + if (r && r > -DBL_MIN && r < DBL_MIN) { + /* Denormal result can be incorrectly rounded here (rather, + truncated). Fixed in newer versions of the C runtime, included + with Windows 11. */ + int original_exp; + frexp(x, &original_exp); + if (original_exp > DBL_MIN_EXP) { + /* Shift down to the smallest normal binade. No bits lost. */ + x = ldexp(x, DBL_MIN_EXP - original_exp); + exp += original_exp - DBL_MIN_EXP; + } + /* Multiplying by 2**exp finishes the job, and the HW will round as + appropriate. Note: if exp < -DBL_MANT_DIG, all of x is shifted + to be < 0.5ULP of smallest denorm, so should be thrown away. If + exp is so very negative that ldexp underflows to 0, that's fine; + no need to check in advance. */ + r = x*ldexp(1.0, (int)exp); + } +#endif if (isinf(r)) errno = ERANGE; } From d3312402db16ded2a49b45b72baa74759739699b Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Tue, 29 Apr 2025 18:55:12 +0300 Subject: [PATCH 2/9] Update Modules/mathmodule.c --- Modules/mathmodule.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Modules/mathmodule.c b/Modules/mathmodule.c index 0ef6290950de23..d9ca99b83aefaf 100644 --- a/Modules/mathmodule.c +++ b/Modules/mathmodule.c @@ -2172,8 +2172,9 @@ math_ldexp_impl(PyObject *module, double x, PyObject *i) frexp(x, &original_exp); if (original_exp > DBL_MIN_EXP) { /* Shift down to the smallest normal binade. No bits lost. */ - x = ldexp(x, DBL_MIN_EXP - original_exp); - exp += original_exp - DBL_MIN_EXP; + int shift = DBL_MIN_EXP - original_exp + x = ldexp(x, shift); + exp -= shift; } /* Multiplying by 2**exp finishes the job, and the HW will round as appropriate. Note: if exp < -DBL_MANT_DIG, all of x is shifted From 1b90e9cd0b88d3a5b1a5b8e928b7a27ae0b86eee Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Tue, 29 Apr 2025 19:18:42 +0300 Subject: [PATCH 3/9] Apply suggestions from code review --- Modules/mathmodule.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Modules/mathmodule.c b/Modules/mathmodule.c index d9ca99b83aefaf..ab7ed1e1bc7362 100644 --- a/Modules/mathmodule.c +++ b/Modules/mathmodule.c @@ -2164,7 +2164,7 @@ math_ldexp_impl(PyObject *module, double x, PyObject *i) errno = 0; r = ldexp(x, (int)exp); #if _MSC_VER - if (r && r > -DBL_MIN && r < DBL_MIN) { + if (-DBL_MIN < r && r < DBL_MIN) { /* Denormal result can be incorrectly rounded here (rather, truncated). Fixed in newer versions of the C runtime, included with Windows 11. */ @@ -2172,7 +2172,7 @@ math_ldexp_impl(PyObject *module, double x, PyObject *i) frexp(x, &original_exp); if (original_exp > DBL_MIN_EXP) { /* Shift down to the smallest normal binade. No bits lost. */ - int shift = DBL_MIN_EXP - original_exp + int shift = DBL_MIN_EXP - original_exp; x = ldexp(x, shift); exp -= shift; } From e1c2f9962f6ef90f08b7651540dbde249bcf3881 Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Tue, 29 Apr 2025 21:26:32 +0300 Subject: [PATCH 4/9] Update Modules/mathmodule.c --- Modules/mathmodule.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/mathmodule.c b/Modules/mathmodule.c index ab7ed1e1bc7362..2fbe288e38cfbf 100644 --- a/Modules/mathmodule.c +++ b/Modules/mathmodule.c @@ -2165,7 +2165,7 @@ math_ldexp_impl(PyObject *module, double x, PyObject *i) r = ldexp(x, (int)exp); #if _MSC_VER if (-DBL_MIN < r && r < DBL_MIN) { - /* Denormal result can be incorrectly rounded here (rather, + /* Denormal (or zero) results can be incorrectly rounded here (rather, truncated). Fixed in newer versions of the C runtime, included with Windows 11. */ int original_exp; From 35bd4575057a1a17dd190c92833b1ff23e4c91ac Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Wed, 30 Apr 2025 08:17:06 +0300 Subject: [PATCH 5/9] Update Modules/mathmodule.c --- Modules/mathmodule.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/mathmodule.c b/Modules/mathmodule.c index 2fbe288e38cfbf..80c534f48277c6 100644 --- a/Modules/mathmodule.c +++ b/Modules/mathmodule.c @@ -2163,7 +2163,7 @@ math_ldexp_impl(PyObject *module, double x, PyObject *i) } else { errno = 0; r = ldexp(x, (int)exp); -#if _MSC_VER +#ifdef _MSC_VER if (-DBL_MIN < r && r < DBL_MIN) { /* Denormal (or zero) results can be incorrectly rounded here (rather, truncated). Fixed in newer versions of the C runtime, included From e105d7646076ddd593e3f06b0b37ee43e525b768 Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Wed, 30 Apr 2025 10:11:39 +0300 Subject: [PATCH 6/9] Apply suggestions from code review --- .../Library/2025-04-29-11-48-46.gh-issue-132876.lyTQGZ.rst | 3 ++- Modules/mathmodule.c | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Misc/NEWS.d/next/Library/2025-04-29-11-48-46.gh-issue-132876.lyTQGZ.rst b/Misc/NEWS.d/next/Library/2025-04-29-11-48-46.gh-issue-132876.lyTQGZ.rst index a28a6a474c39e8..e33fde9ccd4f32 100644 --- a/Misc/NEWS.d/next/Library/2025-04-29-11-48-46.gh-issue-132876.lyTQGZ.rst +++ b/Misc/NEWS.d/next/Library/2025-04-29-11-48-46.gh-issue-132876.lyTQGZ.rst @@ -1 +1,2 @@ -Workaround broken ``ldexp()`` implementation on Windows 10. +`ldexp()` on Windows doesn't round subnormal results before Windows 11, but should. Python's :func:`math.ldexp` wrapper now does round them, so results may change +slightly, in rare cases of very small results, on Windows versions before 11. diff --git a/Modules/mathmodule.c b/Modules/mathmodule.c index 80c534f48277c6..344a744925e0a5 100644 --- a/Modules/mathmodule.c +++ b/Modules/mathmodule.c @@ -2164,7 +2164,7 @@ math_ldexp_impl(PyObject *module, double x, PyObject *i) errno = 0; r = ldexp(x, (int)exp); #ifdef _MSC_VER - if (-DBL_MIN < r && r < DBL_MIN) { + if (DBL_MIN > r && r > -DBL_MIN) { /* Denormal (or zero) results can be incorrectly rounded here (rather, truncated). Fixed in newer versions of the C runtime, included with Windows 11. */ From 514ecc0faa3d7ac7d21e48c95a247d2eed34c36b Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Wed, 30 Apr 2025 10:12:49 +0300 Subject: [PATCH 7/9] Apply suggestions from code review --- .../Library/2025-04-29-11-48-46.gh-issue-132876.lyTQGZ.rst | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Misc/NEWS.d/next/Library/2025-04-29-11-48-46.gh-issue-132876.lyTQGZ.rst b/Misc/NEWS.d/next/Library/2025-04-29-11-48-46.gh-issue-132876.lyTQGZ.rst index e33fde9ccd4f32..67e4adbaadec2b 100644 --- a/Misc/NEWS.d/next/Library/2025-04-29-11-48-46.gh-issue-132876.lyTQGZ.rst +++ b/Misc/NEWS.d/next/Library/2025-04-29-11-48-46.gh-issue-132876.lyTQGZ.rst @@ -1,2 +1,4 @@ -`ldexp()` on Windows doesn't round subnormal results before Windows 11, but should. Python's :func:`math.ldexp` wrapper now does round them, so results may change -slightly, in rare cases of very small results, on Windows versions before 11. +`ldexp()` on Windows doesn't round subnormal results before Windows 11, +but should. Python's :func:`math.ldexp` wrapper now does round them, so +results may change slightly, in rare cases of very small results, on +Windows versions before 11. From ccc262c17c097cc41df9479c42748c97e2c072d3 Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Wed, 30 Apr 2025 10:17:21 +0300 Subject: [PATCH 8/9] Apply suggestions from code review//lint --- .../next/Library/2025-04-29-11-48-46.gh-issue-132876.lyTQGZ.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Library/2025-04-29-11-48-46.gh-issue-132876.lyTQGZ.rst b/Misc/NEWS.d/next/Library/2025-04-29-11-48-46.gh-issue-132876.lyTQGZ.rst index 67e4adbaadec2b..cb3ca3321e3d26 100644 --- a/Misc/NEWS.d/next/Library/2025-04-29-11-48-46.gh-issue-132876.lyTQGZ.rst +++ b/Misc/NEWS.d/next/Library/2025-04-29-11-48-46.gh-issue-132876.lyTQGZ.rst @@ -1,4 +1,4 @@ -`ldexp()` on Windows doesn't round subnormal results before Windows 11, +``ldexp()`` on Windows doesn't round subnormal results before Windows 11, but should. Python's :func:`math.ldexp` wrapper now does round them, so results may change slightly, in rare cases of very small results, on Windows versions before 11. From bd5446f49af71349ba03747d86b30c2f33a1bcc2 Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Mon, 26 May 2025 04:49:29 +0300 Subject: [PATCH 9/9] address review: added regression test --- Lib/test/test_math.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Lib/test/test_math.py b/Lib/test/test_math.py index 913a60bf9e04e3..d14336f8bac498 100644 --- a/Lib/test/test_math.py +++ b/Lib/test/test_math.py @@ -1214,6 +1214,12 @@ def testLdexp(self): self.assertEqual(math.ldexp(NINF, n), NINF) self.assertTrue(math.isnan(math.ldexp(NAN, n))) + @requires_IEEE_754 + def testLdexp_denormal(self): + # Denormal output incorrectly rounded (truncated) + # on some Windows. + self.assertEqual(math.ldexp(6993274598585239, -1126), 1e-323) + def testLog(self): self.assertRaises(TypeError, math.log) self.assertRaises(TypeError, math.log, 1, 2, 3)