Skip to content

gh-157742: Check for signals every 64th iteration in the int arithmetic loops - #157744

Open
eendebakpt wants to merge 2 commits into
python:mainfrom
eendebakpt:longobject-sigcheck-batch
Open

eendebakpt wants to merge 2 commits into
python:mainfrom
eendebakpt:longobject-sigcheck-batch

Conversation

@eendebakpt

@eendebakpt eendebakpt commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

x_mul() checks for signals on every row, x_divrem() on every quotient digit and the decimal string conversion on every digit. PyErr_CheckSignals() costs about as much as a hundred digit operations, so for small operands the checks cost more than the arithmetic.

SIGCHECK() now takes the loop index and checks on every 64th iteration, the same pattern _sre uses.

Benchmark main PR
a * b (10 x 10 digits) 159 ns 97.2 ns: 1.64x faster
a * b (60 x 60 digits) 2.61 us 2.22 us: 1.18x faster
a * b (500 x 500 digits) 99.1 us 79.7 us: 1.24x faster
a // b (1000 // 2 digits) 13.8 us 10.3 us: 1.34x faster
a // b (1000 // 500 digits) 204 us not significant
pow(3, 5000) 10.7 us 8.38 us: 1.28x faster
math.factorial(2000) 97.7 us 81.6 us: 1.20x faster
str(7**1000) 8.07 us 7.34 us: 1.10x faster
Benchmark script
import pyperf

runner = pyperf.Runner()
def ints(na, nb):
    return (f"import random; r = random.Random(1); "
            f"a = r.getrandbits({na*30-1}) | (1 << {na*30-2}); "
            f"b = r.getrandbits({nb*30-1}) | (1 << {nb*30-2})")
runner.timeit("a * b (10 x 10 digits)", "a * b", setup=ints(10, 10))
runner.timeit("a * b (60 x 60 digits)", "a * b", setup=ints(60, 60))
runner.timeit("a * b (500 x 500 digits)", "a * b", setup=ints(500, 500))
runner.timeit("a // b (1000 // 2 digits)", "a // b", setup=ints(1000, 2))
runner.timeit("a // b (1000 // 500 digits)", "a // b", setup=ints(1000, 500))
runner.timeit("pow(3, 5000)", "pow(3, 5000)")
runner.timeit("math.factorial(2000)", "factorial(2000)",
              setup="from math import factorial")
runner.timeit("str(7**1000)", "str(x)", setup="x = 7**1000")

Generated with Claude Code

x_mul() checked for signals on every row, x_divrem() on every quotient
digit and the decimal string conversion on every digit.
PyErr_CheckSignals() costs about as much as a hundred digit operations,
so for small operands (a 2-digit divisor, a 10x10 product) the checks cost
more than the arithmetic.  SIGCHECK() now takes the loop index and checks
on every 64th iteration.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@eendebakpt eendebakpt changed the title heck for signals every 64th iteration in the int arithmetic loops gh-157742: Check for signals every 64th iteration in the int arithmetic loops Sep 18, 2026
@skirpichev

Copy link
Copy Markdown
Member

Could you test pidigits on this pr?

@eendebakpt

Copy link
Copy Markdown
Contributor Author

Could you test pidigits on this pr?

pidigits is in the noise (although on a quiet machine one can measure the improvement I believe). With the same script as above one can see that also the multiplication of 64-bits ints gains.

Benchmark main (run 1 / run 2) PR (run 1 / run 2) Result
a * b (1 x 2 digits) 25.3 / 25.2 ns 23.2 / 23.2 ns 1.09x faster
a * b (2 x 1 digits) 25.5 / 25.7 ns 22.9 / 22.8 ns 1.12x faster
a * b (3 x 1 digits) 25.1 / 25.5 ns 23.4 / 23.3 ns 1.08x faster
pidigits 155 / 155 ms 155 / 155 ms not significant

@skirpichev skirpichev left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@@ -0,0 +1 @@
Improve performance of arithmetic operations like multiplication and division for :class:`int`.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This affects also string conversion.

@picnixz

picnixz commented Sep 20, 2026

Copy link
Copy Markdown
Member

For systems under duress, how long should we wait when doing a ctrl+c for Python to exit properly (is it visible?)

Note to myself and for others in general but did we audit other parts of the code where we spent long calculations without checking for signals? I never really considered this problem but I wonder if hashlib in general should not check that when doing hashing per large blocks. I never worked with the check signal API so I do not know when this applies as well (if we are not holding the GIL should we check for signals?)

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.

3 participants