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 00000000000000..241f3f76698dca --- /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. diff --git a/Modules/mathintegermodule.c b/Modules/mathintegermodule.c index 0f660d461e349f..d5423d5dcd2954 100644 --- a/Modules/mathintegermodule.c +++ b/Modules/mathintegermodule.c @@ -114,41 +114,61 @@ math_integer_lcm_impl(PyObject *module, PyObject * const *args, Py_ssize_t args_length) /*[clinic end generated code: output=3e88889b866ccc28 input=261bddc85a136bdf]*/ { - 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; - } - if (args_length == 1) { - Py_SETREF(res, PyNumber_Absolute(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; + /* Combine intermediate results in size-balanced order: a new value + 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; + int top = 0; + for (Py_ssize_t i = 0; ; i++) { + res = PyNumber_Index(args[i]); + if (res == NULL) { + goto error; } - if (res == zero) { - /* Fast path: just check arguments. - It is okay to use identity comparison here. */ - Py_DECREF(x); - continue; + 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); } - Py_SETREF(res, long_lcm(res, x)); - Py_DECREF(x); + if (i + 1 >= args_length) { + break; + } + assert(top < (int)Py_ARRAY_LENGTH(stack)); + sizes[top] = dres; + stack[top++] = res; + } + while (top > 0) { + top--; + Py_SETREF(res, long_lcm(res, stack[top])); + Py_DECREF(stack[top]); if (res == NULL) { - return NULL; + goto error; } } + if (args_length == 1) { + Py_SETREF(res, PyNumber_Absolute(res)); + } return res; + +error: + while (top > 0) { + top--; + Py_DECREF(stack[top]); + } + return NULL; }