Skip to content

Commit 430e476

Browse files
committed
bpo-43420: Simple optimizations for Fraction's arithmetics
making Fraction class more usable for large arguments (>~ 10**6), with cost 10-20% for small components case. Before: $ ./python -m timeit -s 'from fractions import Fraction as F' \ -s 'a=[F(1, _**3) for _ in range(1, 1000)]' 'sum(a)' 5 loops, best of 5: 81.2 msec per loop After: $ ./python -m timeit -s 'from fractions import Fraction as F' \ -s 'a=[F(1, _**3) for _ in range(1, 1000)]' 'sum(a)' 10 loops, best of 5: 23 msec per loop References: Knuth, TAOCP, Volume 2, 4.5.1, https://www.eecis.udel.edu/~saunders/courses/822/98f/collins-notes/rnarith.ps, https://gmplib.org/ (e.g. https://gmplib.org/repo/gmp/file/tip/mpq/aors.c)
1 parent 598b3c9 commit 430e476

File tree

2 files changed

+89
-6
lines changed

2 files changed

+89
-6
lines changed

Lib/fractions.py

Lines changed: 87 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -381,10 +381,79 @@ def reverse(b, a):
381381

382382
return forward, reverse
383383

384+
# Rational arithmetic algorithms: Knuth, TAOCP, Volume 2, 4.5.1.
385+
#
386+
# Assume input fractions a and b are normalized.
387+
#
388+
# 1) Consider addition/substraction.
389+
#
390+
# Let g = gcd(da, db). Then
391+
#
392+
# na nb na*db ± nb*da
393+
# a ± b == -- ± -- == ------------- ==
394+
# da db da*db
395+
#
396+
# na*(db//g) ± nb*(da//g) t
397+
# == ----------------------- == -
398+
# (da*db)//g d
399+
#
400+
# Now, if g > 1, we're working with smaller integers.
401+
#
402+
# Note, that t, (da//g) and (db//g) are pairwise coprime.
403+
#
404+
# Indeed, (da//g) and (db//g) share no common factors (they were
405+
# removed) and da is coprime with na (since input fractions are
406+
# normalized), hence (da//g) and na are coprime. By symmetry,
407+
# (db//g) and nb are coprime too. Then,
408+
#
409+
# gcd(t, da//g) == gcd(na*(db//g), da//g) == 1
410+
# gcd(t, db//g) == gcd(nb*(da//g), db//g) == 1
411+
#
412+
# Above allows us optimize reduction of the result to lowest
413+
# terms. Indeed,
414+
#
415+
# g2 = gcd(t, d) == gcd(t, (da//g)*(db//g)*g) == gcd(t, g)
416+
#
417+
# t//g2 t//g2
418+
# a ± b == ----------------------- == ----------------
419+
# (da//g)*(db//g)*(g//g2) (da//g)*(db//g2)
420+
#
421+
# is a normalized fraction. This is useful because the unnormalized
422+
# denominator d could be much larger than g.
423+
#
424+
# We should special-case g == 1, since 60.8% of randomly-chosen
425+
# integers are coprime:
426+
# https://en.wikipedia.org/wiki/Coprime_integers#Probability_of_coprimality
427+
#
428+
# 2) Consider multiplication
429+
#
430+
# Let g1 = gcd(na, db) and g2 = gcd(nb, da), then
431+
#
432+
# na*nb na*nb (na//g1)*(nb//g2)
433+
# a*b == ----- == ----- == -----------------
434+
# da*db db*da (db//g1)*(da//g2)
435+
#
436+
# Note, that after divisions we're multiplying smaller integers.
437+
#
438+
# Also, the resulting fraction is normalized, because each of
439+
# two factors in the numerator is coprime to each of the two factors
440+
# in the denominator.
441+
#
442+
# Indeed, pick (na//g1). It's coprime with (da//g2), because input
443+
# fractions are normalized. It's also coprime with (db//g1), because
444+
# common factors are removed by g1 == gcd(na, db).
445+
384446
def _add_sub_(a, b, pm=int.__add__):
385-
da, db = a.denominator, b.denominator
386-
return Fraction(pm(a.numerator * db, b.numerator * da),
387-
da * db)
447+
na, da = a.numerator, a.denominator
448+
nb, db = b.numerator, b.denominator
449+
g = math.gcd(da, db)
450+
if g == 1:
451+
return Fraction(pm(na * db, da * nb), da * db, _normalize=False)
452+
else:
453+
s = da // g
454+
t = pm(na * (db // g), nb * s)
455+
g2 = math.gcd(t, g)
456+
return Fraction(t // g2, s * (db // g2), _normalize=False)
388457

389458
_add = functools.partial(_add_sub_)
390459
_add.__doc__ = 'a + b'
@@ -396,14 +465,26 @@ def _add_sub_(a, b, pm=int.__add__):
396465

397466
def _mul(a, b):
398467
"""a * b"""
399-
return Fraction(a.numerator * b.numerator, a.denominator * b.denominator)
468+
na, da = a.numerator, a.denominator
469+
nb, db = b.numerator, b.denominator
470+
g1 = math.gcd(na, db)
471+
g2 = math.gcd(nb, da)
472+
return Fraction((na // g1) * (nb // g2),
473+
(db // g1) * (da // g2), _normalize=False)
400474

401475
__mul__, __rmul__ = _operator_fallbacks(_mul, operator.mul)
402476

403477
def _div(a, b):
404478
"""a / b"""
405-
return Fraction(a.numerator * b.denominator,
406-
a.denominator * b.numerator)
479+
# Same as _mul(), with inversed b.
480+
na, da = a.numerator, a.denominator
481+
nb, db = b.numerator, b.denominator
482+
g1 = math.gcd(na, nb)
483+
g2 = math.gcd(db, da)
484+
n, d = (na // g1) * (db // g2), (nb // g1) * (da // g2)
485+
if nb < 0:
486+
n, d = -n, -d
487+
return Fraction(n, d, _normalize=False)
407488

408489
__truediv__, __rtruediv__ = _operator_fallbacks(_div, operator.truediv)
409490

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Improve performance of class:`fractions.Fraction` arithmetics for large
2+
components.

0 commit comments

Comments
 (0)