Skip to content

gh-102221: Optimize math.integer.lcm() for multiple arguments (size-guarded stack)#153358

Open
serhiy-storchaka wants to merge 8 commits into
python:mainfrom
serhiy-storchaka:math-lcm-size-guarded
Open

gh-102221: Optimize math.integer.lcm() for multiple arguments (size-guarded stack)#153358
serhiy-storchaka wants to merge 8 commits into
python:mainfrom
serhiy-storchaka:math-lcm-size-guarded

Conversation

@serhiy-storchaka

@serhiy-storchaka serhiy-storchaka commented Jul 8, 2026

Copy link
Copy Markdown
Member

Combine intermediate results in size-balanced order instead of a sequential fold: a new value is merged with stacked intermediate results while it has at least 3/4 as many digits as the top stack entry, otherwise it is pushed.

Entry sizes thus grow at least 4/3 times per stack entry, which bounds the stack depth by log(PY_SSIZE_T_MAX)/log(4/3) + 1: 153 entries on 64-bit and 76 on 32-bit platforms.

Unlike merging at predefined points (recursive halving in #135554, binary counter in #135609), the merge order adapts to the actual sizes of intermediate results: small arguments are combined with each other before touching a much larger partial result. This helps in particular when a few large arguments are mixed with many small ones and gcd cancellation keeps the partial results small — the large values then participate in a single merge instead of ~log(n) merges.

Benchmarks (non-debug build, x86-64):

arguments main #135554 #135609 this PR
1000 × 64-bit 14.9 ms 2.42 ms 2.44 ms 2.44 ms
3 × 100k-bit + 3000 × 30-bit 133 ms 46.8 ms 47.6 ms 46.5 ms
1 × 2M-bit + 5000 small repeated 1235 ms 5.9 ms 5.0 ms 0.96 ms
20 args, sizes 2^20..2 bits descending 1345 ms 1237 ms 1263 ms 875 ms

Alternative to #135554 and #135609.

serhiy-storchaka and others added 8 commits August 12, 2025 21:20
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
@skirpichev

Copy link
Copy Markdown
Member

I suspect this will be slightly worse for small number of arguments. Do we have measurable difference wrt the main here?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants