Skip to content

Commit 2298094

Browse files
committed
Add _pylong.py module.
Add Python implementations of certain longobject.c functions. These use asymptotically faster algorithms that can be used for operations on integers with many digits. In those cases, the performance overhead of the Python implementation is not significant since the asymptotic behavior is what dominates runtime. Functions provided by this module should be considered private and not part of any public API. Co-author: Tim Peters <[email protected]> Co-author: Mark Dickinson <[email protected]> Co-author: Bjorn Martinsson
1 parent 05c9275 commit 2298094

File tree

5 files changed

+467
-0
lines changed

5 files changed

+467
-0
lines changed

Lib/_pylong.py

Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
"""Python implementations of some algorithms for use by longobject.c.
2+
The goal is to provide asymptotically faster algorithms that can be
3+
used for operations on integers with many digits. In those cases, the
4+
performance overhead of the Python implementation is not significant
5+
since the asymptotic behavior is what dominates runtime. Functions
6+
provided by this module should be considered private and not part of any
7+
public API.
8+
9+
Note: for ease of maintainability, please prefer clear code and avoid
10+
"micro-optimizations". This module will only be imported and used for
11+
integers with a huge number of digits. Saving a few microseconds with
12+
tricky or non-obvious code is not worth it. For people looking for
13+
maximum performance, they should use something like gmpy2."""
14+
15+
import sys
16+
import re
17+
import decimal
18+
19+
_DEBUG = False
20+
21+
22+
def int_to_decimal(n):
23+
"""Asymptotically fast conversion of an 'int' to Decimal."""
24+
25+
# Function due to Tim Peters. See GH issue #90716 for details.
26+
# https://github.com/python/cpython/issues/90716
27+
#
28+
# The implementation in longobject.c of base conversion algorithms
29+
# between power-of-2 and non-power-of-2 bases are quadratic time.
30+
# This function implements a divide-and-conquer algorithm that is
31+
# faster for large numbers. Builds an equal decimal.Decimal in a
32+
# "clever" recursive way. If we want a string representation, we
33+
# apply str to _that_.
34+
35+
if _DEBUG:
36+
print('int_to_decimal', n.bit_length(), file=sys.stderr)
37+
38+
D = decimal.Decimal
39+
D2 = D(2)
40+
41+
BITLIM = 128
42+
43+
mem = {}
44+
45+
def w2pow(w):
46+
"""Return D(2)**w and store the result. Also possibly save some
47+
intermediate results. In context, these are likely to be reused
48+
across various levels of the conversion to Decimal."""
49+
if (result := mem.get(w)) is None:
50+
if w <= BITLIM:
51+
result = D2**w
52+
elif w - 1 in mem:
53+
result = (t := mem[w - 1]) + t
54+
else:
55+
w2 = w >> 1
56+
# If w happens to be odd, w-w2 is one larger then w2
57+
# now. Recurse on the smaller first (w2), so that it's
58+
# in the cache and the larger (w-w2) can be handled by
59+
# the cheaper `w-1 in mem` branch instead.
60+
result = w2pow(w2) * w2pow(w - w2)
61+
mem[w] = result
62+
return result
63+
64+
def inner(n, w):
65+
if w <= BITLIM:
66+
return D(n)
67+
w2 = w >> 1
68+
hi = n >> w2
69+
lo = n - (hi << w2)
70+
return inner(lo, w2) + inner(hi, w - w2) * w2pow(w2)
71+
72+
with decimal.localcontext() as ctx:
73+
ctx.prec = decimal.MAX_PREC
74+
ctx.Emax = decimal.MAX_EMAX
75+
ctx.Emin = decimal.MIN_EMIN
76+
ctx.traps[decimal.Inexact] = 1
77+
78+
if n < 0:
79+
negate = True
80+
n = -n
81+
else:
82+
negate = False
83+
result = inner(n, n.bit_length())
84+
if negate:
85+
result = -result
86+
return result
87+
88+
89+
def int_to_decimal_string(n):
90+
"""Asymptotically fast conversion of an 'int' to a decimal string."""
91+
return str(int_to_decimal(n))
92+
93+
94+
def _str_to_int_inner(s):
95+
"""Asymptotically fast conversion of a 'str' to an 'int'."""
96+
97+
# Function due to Bjorn Martinsson. See GH issue #90716 for details.
98+
# https://github.com/python/cpython/issues/90716
99+
#
100+
# The implementation in longobject.c of base conversion algorithms
101+
# between power-of-2 and non-power-of-2 bases are quadratic time.
102+
# This function implements a divide-and-conquer algorithm making use
103+
# of Python's built in big int multiplication. Since Python uses the
104+
# Karatsuba algorithm for multiplication, the time complexity
105+
# of this function is O(len(s)**1.58).
106+
107+
DIGLIM = 2048
108+
109+
mem = {}
110+
111+
def w5pow(w):
112+
"""Return 5**w and store the result.
113+
Also possibly save some intermediate results. In context, these
114+
are likely to be reused across various levels of the conversion
115+
to 'int'.
116+
"""
117+
if (result := mem.get(w)) is None:
118+
if w <= DIGLIM:
119+
result = 5**w
120+
elif w - 1 in mem:
121+
result = mem[w - 1] * 5
122+
else:
123+
w2 = w >> 1
124+
# If w happens to be odd, w-w2 is one larger then w2
125+
# now. Recurse on the smaller first (w2), so that it's
126+
# in the cache and the larger (w-w2) can be handled by
127+
# the cheaper `w-1 in mem` branch instead.
128+
result = w5pow(w2) * w5pow(w - w2)
129+
mem[w] = result
130+
return result
131+
132+
def inner(a, b):
133+
if b - a <= DIGLIM:
134+
return int(s[a:b])
135+
mid = (a + b + 1) >> 1
136+
return inner(mid, b) + ((inner(a, mid) * w5pow(b - mid)) << (b - mid))
137+
138+
return inner(0, len(s))
139+
140+
141+
def int_from_string(s):
142+
"""Asymptotically fast version of PyLong_FromString(), conversion
143+
of a string of decimal digits into an 'int'."""
144+
if _DEBUG:
145+
print('int_from_string', len(s), file=sys.stderr)
146+
# PyLong_FromString() has already removed leading +/-, checked for invalid
147+
# use of underscore characters, checked that string consists of only digits
148+
# and underscores, and stripped leading whitespace. The input can still
149+
# contain underscores and have trailing whitespace.
150+
s = s.rstrip().replace('_', '')
151+
return _str_to_int_inner(s)
152+
153+
154+
def str_to_int(s):
155+
"""Asymptotically fast version of decimal string to 'int' conversion."""
156+
# FIXME: this doesn't support the full syntax that int() supports.
157+
m = re.match(r'\s*([+-]?)([0-9_]+)\s*', s)
158+
if not m:
159+
raise ValueError('invalid literal for int() with base 10')
160+
v = int_from_string(m.group(2))
161+
if m.group(1) == '-':
162+
v = -v
163+
return v
164+
165+
166+
# Fast integer division, based on code from Mark Dickinson, fast_div.py
167+
# GH-47701. The algorithm is due to Burnikel and Ziegler, in their paper
168+
# "Fast Recursive Division".
169+
170+
_DIV_LIMIT = 1000
171+
172+
173+
def _div2n1n(a, b, n):
174+
"""Divide a 2n-bit nonnegative integer a by an n-bit positive integer
175+
b, using a recursive divide-and-conquer algorithm.
176+
177+
Inputs:
178+
n is a positive integer
179+
b is a positive integer with exactly n bits
180+
a is a nonnegative integer such that a < 2**n * b
181+
182+
Output:
183+
(q, r) such that a = b*q+r and 0 <= r < b.
184+
185+
"""
186+
if n <= _DIV_LIMIT:
187+
return divmod(a, b)
188+
pad = n & 1
189+
if pad:
190+
a <<= 1
191+
b <<= 1
192+
n += 1
193+
half_n = n >> 1
194+
mask = (1 << half_n) - 1
195+
b1, b2 = b >> half_n, b & mask
196+
q1, r = _div3n2n(a >> n, (a >> half_n) & mask, b, b1, b2, half_n)
197+
q2, r = _div3n2n(r, a & mask, b, b1, b2, half_n)
198+
if pad:
199+
r >>= 1
200+
return q1 << half_n | q2, r
201+
202+
203+
def _div3n2n(a12, a3, b, b1, b2, n):
204+
"""Helper function for _div2n1n; not intended to be called directly."""
205+
if a12 >> n == b1:
206+
q, r = (1 << n) - 1, a12 - (b1 << n) + b1
207+
else:
208+
q, r = _div2n1n(a12, b1, n)
209+
r = (r << n | a3) - q * b2
210+
while r < 0:
211+
q -= 1
212+
r += b
213+
return q, r
214+
215+
216+
def _divmod_pos(a, b):
217+
"""Divide a positive integer a by a positive integer b, giving
218+
quotient and remainder."""
219+
# Use grade-school algorithm in base 2**n, n = nbits(b)
220+
n = b.bit_length()
221+
mask = (1 << n) - 1
222+
a_digits = []
223+
while a:
224+
a_digits.append(a & mask)
225+
a >>= n
226+
r = 0 if a_digits[-1] >= b else a_digits.pop()
227+
q = 0
228+
while a_digits:
229+
q_digit, r = _div2n1n((r << n) + a_digits.pop(), b, n)
230+
q = (q << n) + q_digit
231+
return q, r
232+
233+
234+
def int_divmod(a, b):
235+
"""Asymptotically fast replacement for divmod, for 'int'."""
236+
if _DEBUG:
237+
print('int_divmod', a.bit_length(), b.bit_length(), file=sys.stderr)
238+
if b == 0:
239+
raise ZeroDivisionError
240+
elif b < 0:
241+
q, r = int_divmod(-a, -b)
242+
return q, -r
243+
elif a < 0:
244+
q, r = int_divmod(~a, b)
245+
return ~q, b + ~r
246+
elif a == 0:
247+
return 0, 0
248+
else:
249+
return _divmod_pos(a, b)

Lib/test/test_int.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -775,5 +775,52 @@ class IntSubclassStrDigitLimitsTests(IntStrDigitLimitsTests):
775775
int_class = IntSubclass
776776

777777

778+
class PyLongModuleTests(unittest.TestCase):
779+
# Tests of the functions in _pylong.py. Those get used when the
780+
# number of digits in the input values are large enough.
781+
782+
def setUp(self):
783+
super().setUp()
784+
self._previous_limit = sys.get_int_max_str_digits()
785+
sys.set_int_max_str_digits(0)
786+
787+
def tearDown(self):
788+
sys.set_int_max_str_digits(self._previous_limit)
789+
super().tearDown()
790+
791+
def test_pylong_int_to_decimal(self):
792+
n = (1 << 100_000) - 1
793+
suffix = '9883109375'
794+
s = str(n)
795+
assert s[-10:] == suffix
796+
s = str(-n)
797+
assert s[-10:] == suffix
798+
s = '%d' % n
799+
assert s[-10:] == suffix
800+
s = b'%d' % n
801+
assert s[-10:] == suffix.encode('ascii')
802+
803+
def test_pylong_int_divmod(self):
804+
n = (1 << 100_000)
805+
a, b = divmod(n*3 + 1, n)
806+
assert a == 3 and b == 1
807+
808+
def test_pylong_str_to_int(self):
809+
v1 = 1 << 100_000
810+
s = str(v1)
811+
v2 = int(s)
812+
assert v1 == v2
813+
v3 = int(' -' + s)
814+
assert -v1 == v3
815+
v4 = int(' +' + s + ' ')
816+
assert v1 == v4
817+
with self.assertRaises(ValueError) as err:
818+
int(s + 'z')
819+
with self.assertRaises(ValueError) as err:
820+
int(s + '_')
821+
with self.assertRaises(ValueError) as err:
822+
int('_' + s)
823+
824+
778825
if __name__ == "__main__":
779826
unittest.main()
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Add _pylong.py module. It includes asymptotically faster algorithms that
2+
can be used for operations on integers with many digits. It is used by
3+
longobject.c to speed up some operations.

0 commit comments

Comments
 (0)