From 78731c01167604cbd60dcfd895669760fa8f5609 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Mon, 16 Jun 2025 11:46:37 +0300 Subject: [PATCH 1/5] gh-102221: Optimize math.lcm() for multiple arguments --- Modules/mathmodule.c | 57 ++++++++++++++++++++++++-------------------- 1 file changed, 31 insertions(+), 26 deletions(-) diff --git a/Modules/mathmodule.c b/Modules/mathmodule.c index 7c2a421dd6a450c..5bab94d1ed84bb2 100644 --- a/Modules/mathmodule.c +++ b/Modules/mathmodule.c @@ -802,41 +802,46 @@ math_lcm_impl(PyObject *module, PyObject * const *args, Py_ssize_t args_length) /*[clinic end generated code: output=c8a59a5c2e55c816 input=3e4f4b7cdf948a98]*/ { - PyObject *res, *x; - Py_ssize_t i; - if (args_length == 0) { return PyLong_FromLong(1); } - res = PyNumber_Index(args[0]); - if (res == NULL) { - return NULL; + PyObject *res; + PyObject *stack[8 * sizeof(Py_ssize_t)]; + int top = 0; + Py_ssize_t i = 0; + while (1) { + size_t j = i; + res = PyNumber_Index(args[i++]); + if (res == NULL) { + goto error; + } + if (i >= args_length) { + j = ((size_t)1 << top) - 1; + } + for (; j & 1; j >>= 1) { + top--; + Py_SETREF(res, long_lcm(res, stack[top])); + if (res == NULL) { + goto error; + } + Py_DECREF(stack[top]); + } + if (i >= args_length) { + break; + } + stack[top++] = res; } if (args_length == 1) { Py_SETREF(res, PyNumber_Absolute(res)); - return res; } + return res; - PyObject *zero = _PyLong_GetZero(); // borrowed ref - for (i = 1; i < args_length; i++) { - x = PyNumber_Index(args[i]); - if (x == NULL) { - Py_DECREF(res); - return NULL; - } - if (res == zero) { - /* Fast path: just check arguments. - It is okay to use identity comparison here. */ - Py_DECREF(x); - continue; - } - Py_SETREF(res, long_lcm(res, x)); - Py_DECREF(x); - if (res == NULL) { - return NULL; - } +error: + while (top > 0) { + top--; + Py_DECREF(stack[top]); } - return res; + return NULL; } From 9cd2b5f18e362abec804f26995c24d180042bf0d Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Sun, 26 Apr 2026 12:00:33 +0300 Subject: [PATCH 2/5] Add a NEWS entry. --- .../next/Library/2026-04-26-11-47-17.gh-issue-102221.m0wyOh.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2026-04-26-11-47-17.gh-issue-102221.m0wyOh.rst diff --git a/Misc/NEWS.d/next/Library/2026-04-26-11-47-17.gh-issue-102221.m0wyOh.rst b/Misc/NEWS.d/next/Library/2026-04-26-11-47-17.gh-issue-102221.m0wyOh.rst new file mode 100644 index 000000000000000..241f3f76698dcad --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-04-26-11-47-17.gh-issue-102221.m0wyOh.rst @@ -0,0 +1 @@ +Optimize :func:`math.integer.lcm` for multiple arguments. From 20c690c83bf032da0a95169fe4c95ae9e7a03c72 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Wed, 8 Jul 2026 17:54:49 +0300 Subject: [PATCH 3/5] Fix possible leak (on memory error). --- Modules/mathintegermodule.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/mathintegermodule.c b/Modules/mathintegermodule.c index 15f12a648fd30c6..0917fe8ccc2e11d 100644 --- a/Modules/mathintegermodule.c +++ b/Modules/mathintegermodule.c @@ -133,10 +133,10 @@ math_integer_lcm_impl(PyObject *module, PyObject * const *args, for (; j & 1; j >>= 1) { top--; Py_SETREF(res, long_lcm(res, stack[top])); + Py_DECREF(stack[top]); if (res == NULL) { goto error; } - Py_DECREF(stack[top]); } if (i >= args_length) { break; From 1ee08c80f3985a2c3109028236585e2c6b41c08f Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Wed, 8 Jul 2026 18:22:45 +0300 Subject: [PATCH 4/5] gh-102221: Use a size-guarded stack in math.integer.lcm() Instead of merging intermediate results at fixed points (binary counter), merge a new value with stacked values while it has at least half as many digits, so every stack entry has more than twice as many digits as the one above it. Small arguments are combined with each other before touching a much larger partial result. Also fix a reference leak on the error path of the merge loop. Co-Authored-By: Claude Fable 5 --- Modules/mathintegermodule.c | 32 +++++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/Modules/mathintegermodule.c b/Modules/mathintegermodule.c index 578165cb9d0aec9..3473254d3e41dcb 100644 --- a/Modules/mathintegermodule.c +++ b/Modules/mathintegermodule.c @@ -117,20 +117,25 @@ math_integer_lcm_impl(PyObject *module, PyObject * const *args, if (args_length == 0) { return PyLong_FromLong(1); } + /* Combine intermediate results in size-balanced order: a new value + is merged with stacked values while it has at least half as many + digits, so every stack entry has more than twice as many digits + as the one above it. Small arguments are thus combined with each + other before touching a much larger partial result. The doubling + invariant bounds the stack depth by the bit width of the maximal + digit count, so the stack cannot overflow. */ PyObject *res; PyObject *stack[8 * sizeof(Py_ssize_t)]; int top = 0; - Py_ssize_t i = 0; - while (1) { - size_t j = i; - res = PyNumber_Index(args[i++]); + for (Py_ssize_t i = 0; ; i++) { + res = PyNumber_Index(args[i]); if (res == NULL) { goto error; } - if (i >= args_length) { - j = ((size_t)1 << top) - 1; - } - for (; j & 1; j >>= 1) { + while (top > 0 + && (_PyLong_DigitCount((PyLongObject *)res) + >= _PyLong_DigitCount((PyLongObject *)stack[top-1]) / 2)) + { top--; Py_SETREF(res, long_lcm(res, stack[top])); Py_DECREF(stack[top]); @@ -138,11 +143,20 @@ math_integer_lcm_impl(PyObject *module, PyObject * const *args, goto error; } } - if (i >= args_length) { + if (i + 1 >= args_length) { break; } + assert(top < (int)Py_ARRAY_LENGTH(stack)); stack[top++] = res; } + while (top > 0) { + top--; + Py_SETREF(res, long_lcm(res, stack[top])); + Py_DECREF(stack[top]); + if (res == NULL) { + goto error; + } + } if (args_length == 1) { Py_SETREF(res, PyNumber_Absolute(res)); } From 332d60822dacbc8708b16bf558fbf122ba3d694d Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Wed, 8 Jul 2026 19:34:32 +0300 Subject: [PATCH 5/5] Tune the merge threshold and stack size Merge a new value with stacked values while it has at least 3/4 (instead of 1/2) as many digits: this matches the balanced pairing of equal-sized arguments while keeping the size adaptivity, and removes the slowdown for many equal-sized arguments. Cache the digit counts of stacked values in a parallel array. Since entry sizes now grow at least 4/3 times (instead of 2 times) per entry, enlarge the stack from 8*sizeof(Py_ssize_t) to 24*sizeof(Py_ssize_t) entries, which covers the new depth bound of log(PY_SSIZE_T_MAX)/log(4/3) + 1. Co-Authored-By: Claude Fable 5 --- Modules/mathintegermodule.c | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/Modules/mathintegermodule.c b/Modules/mathintegermodule.c index 3473254d3e41dcb..d5423d5dcd29541 100644 --- a/Modules/mathintegermodule.c +++ b/Modules/mathintegermodule.c @@ -118,35 +118,36 @@ math_integer_lcm_impl(PyObject *module, PyObject * const *args, return PyLong_FromLong(1); } /* Combine intermediate results in size-balanced order: a new value - is merged with stacked values while it has at least half as many - digits, so every stack entry has more than twice as many digits - as the one above it. Small arguments are thus combined with each - other before touching a much larger partial result. The doubling - invariant bounds the stack depth by the bit width of the maximal - digit count, so the stack cannot overflow. */ + is merged with stacked values while it has at least 3/4 as many + digits, so the sizes of stack entries grow at least 4/3 times + per entry. Small arguments are thus combined with each other + before touching a much larger partial result. The stack depth + is bounded by log(PY_SSIZE_T_MAX)/log(4/3) plus one possible + zero entry on top: 153 on 64-bit and 76 on 32-bit platforms. */ + PyObject *stack[24 * sizeof(Py_ssize_t)]; + Py_ssize_t sizes[24 * sizeof(Py_ssize_t)]; PyObject *res; - PyObject *stack[8 * sizeof(Py_ssize_t)]; int top = 0; for (Py_ssize_t i = 0; ; i++) { res = PyNumber_Index(args[i]); if (res == NULL) { goto error; } - while (top > 0 - && (_PyLong_DigitCount((PyLongObject *)res) - >= _PyLong_DigitCount((PyLongObject *)stack[top-1]) / 2)) - { + Py_ssize_t dres = _PyLong_DigitCount((PyLongObject *)res); + while (top > 0 && dres >= sizes[top-1] - sizes[top-1] / 4) { top--; Py_SETREF(res, long_lcm(res, stack[top])); Py_DECREF(stack[top]); if (res == NULL) { goto error; } + dres = _PyLong_DigitCount((PyLongObject *)res); } if (i + 1 >= args_length) { break; } assert(top < (int)Py_ARRAY_LENGTH(stack)); + sizes[top] = dres; stack[top++] = res; } while (top > 0) {