Skip to content

Commit 690aca7

Browse files
skirpichevtim-onemdickinson
authored
bpo-43420: Simple optimizations for Fraction's arithmetics (GH-24779)
bpo-43420: Implement standard transformations in + - * / that can often reduce the size of intermediate integers needed. For rationals with large components, this can yield dramatic speed improvements, but for small rationals can run 10-20% slower, due to increased fixed overheads in the longer-winded code. If those slowdowns turn out to be a problem, see the PR discussion for low-level implementation tricks that could cut other fixed overheads. Co-authored-by: Tim Peters <[email protected]> Co-authored-by: Mark Dickinson <[email protected]>
1 parent 9a50ef4 commit 690aca7

File tree

4 files changed

+121
-9
lines changed

4 files changed

+121
-9
lines changed

Lib/fractions.py

Lines changed: 116 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -380,32 +380,139 @@ def reverse(b, a):
380380

381381
return forward, reverse
382382

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

389465
__add__, __radd__ = _operator_fallbacks(_add, operator.add)
390466

391467
def _sub(a, b):
392468
"""a - b"""
393-
da, db = a.denominator, b.denominator
394-
return Fraction(a.numerator * db - b.numerator * da,
395-
da * db)
469+
na, da = a.numerator, a.denominator
470+
nb, db = b.numerator, b.denominator
471+
g = math.gcd(da, db)
472+
if g == 1:
473+
return Fraction(na * db - da * nb, da * db, _normalize=False)
474+
s = da // g
475+
t = na * (db // g) - nb * s
476+
g2 = math.gcd(t, g)
477+
if g2 == 1:
478+
return Fraction(t, s * db, _normalize=False)
479+
return Fraction(t // g2, s * (db // g2), _normalize=False)
396480

397481
__sub__, __rsub__ = _operator_fallbacks(_sub, operator.sub)
398482

399483
def _mul(a, b):
400484
"""a * b"""
401-
return Fraction(a.numerator * b.numerator, a.denominator * b.denominator)
485+
na, da = a.numerator, a.denominator
486+
nb, db = b.numerator, b.denominator
487+
g1 = math.gcd(na, db)
488+
if g1 > 1:
489+
na //= g1
490+
db //= g1
491+
g2 = math.gcd(nb, da)
492+
if g2 > 1:
493+
nb //= g2
494+
da //= g2
495+
return Fraction(na * nb, db * da, _normalize=False)
402496

403497
__mul__, __rmul__ = _operator_fallbacks(_mul, operator.mul)
404498

405499
def _div(a, b):
406500
"""a / b"""
407-
return Fraction(a.numerator * b.denominator,
408-
a.denominator * b.numerator)
501+
# Same as _mul(), with inversed b.
502+
na, da = a.numerator, a.denominator
503+
nb, db = b.numerator, b.denominator
504+
g1 = math.gcd(na, nb)
505+
if g1 > 1:
506+
na //= g1
507+
nb //= g1
508+
g2 = math.gcd(db, da)
509+
if g2 > 1:
510+
da //= g2
511+
db //= g2
512+
n, d = na * db, nb * da
513+
if d < 0:
514+
n, d = -n, -d
515+
return Fraction(n, d, _normalize=False)
409516

410517
__truediv__, __rtruediv__ = _operator_fallbacks(_div, operator.truediv)
411518

Lib/test/test_fractions.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,7 +369,9 @@ def testArithmetic(self):
369369
self.assertEqual(F(1, 2), F(1, 10) + F(2, 5))
370370
self.assertEqual(F(-3, 10), F(1, 10) - F(2, 5))
371371
self.assertEqual(F(1, 25), F(1, 10) * F(2, 5))
372+
self.assertEqual(F(5, 6), F(2, 3) * F(5, 4))
372373
self.assertEqual(F(1, 4), F(1, 10) / F(2, 5))
374+
self.assertEqual(F(-15, 8), F(3, 4) / F(-2, 5))
373375
self.assertTypedEquals(2, F(9, 10) // F(2, 5))
374376
self.assertTypedEquals(10**23, F(10**23, 1) // F(1))
375377
self.assertEqual(F(5, 6), F(7, 3) % F(3, 2))

Misc/ACKS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -902,6 +902,7 @@ James King
902902
W. Trevor King
903903
Jeffrey Kintscher
904904
Paul Kippes
905+
Sergey B Kirpichev
905906
Steve Kirsch
906907
Sebastian Kirsche
907908
Kamil Kisiel
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. Contributed by Sergey B. Kirpichev.

0 commit comments

Comments
 (0)