Skip to content

Commit 3a374e0

Browse files
scoderserhiy-storchaka
authored andcommitted
bpo-35588: Speed up mod, divmod and floordiv operations for Fraction type (#11322)
* bpo-35588: Implement mod and divmod operations for Fraction type by spelling out the numerator/denominator calculation, instead of instantiating and normalising Fractions along the way. This speeds up '%' and divmod() by 2-3x. * bpo-35588: Also reimplement Fraction.__floordiv__() using integer operations to make it ~4x faster. * Improve code formatting. Co-Authored-By: scoder <[email protected]> * bpo-35588: Fix return type of divmod(): the result of the integer division should be an integer. * bpo-35588: Further specialise __mod__() and inline the original helper function _flat_divmod() since it's no longer reused. * bpo-35588: Add some tests with large numerators and/or denominators. * bpo-35588: Use builtin "divmod()" function for implementing __divmod__() in order to simplify the implementation, even though performance results are mixed. * Rremove accidentally added empty line. * bpo-35588: Try to provide more informative output on test failures. * bpo-35588: Improve wording in News entry. Co-Authored-By: scoder <[email protected]> * Remove stray space.
1 parent a1d1425 commit 3a374e0

File tree

3 files changed

+63
-4
lines changed

3 files changed

+63
-4
lines changed

Lib/fractions.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -429,14 +429,22 @@ def _div(a, b):
429429

430430
def _floordiv(a, b):
431431
"""a // b"""
432-
return math.floor(a / b)
432+
return (a.numerator * b.denominator) // (a.denominator * b.numerator)
433433

434434
__floordiv__, __rfloordiv__ = _operator_fallbacks(_floordiv, operator.floordiv)
435435

436+
def _divmod(a, b):
437+
"""(a // b, a % b)"""
438+
da, db = a.denominator, b.denominator
439+
div, n_mod = divmod(a.numerator * db, da * b.numerator)
440+
return div, Fraction(n_mod, da * db)
441+
442+
__divmod__, __rdivmod__ = _operator_fallbacks(_divmod, divmod)
443+
436444
def _mod(a, b):
437445
"""a % b"""
438-
div = a // b
439-
return a - b * div
446+
da, db = a.denominator, b.denominator
447+
return Fraction((a.numerator * db) % (b.numerator * da), da * db)
440448

441449
__mod__, __rmod__ = _operator_fallbacks(_mod, operator.mod)
442450

Lib/test/test_fractions.py

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,11 @@ def assertTypedEquals(self, expected, actual):
117117
self.assertEqual(type(expected), type(actual))
118118
self.assertEqual(expected, actual)
119119

120+
def assertTypedTupleEquals(self, expected, actual):
121+
"""Asserts that both the types and values in the tuples are the same."""
122+
self.assertTupleEqual(expected, actual)
123+
self.assertListEqual(list(map(type, expected)), list(map(type, actual)))
124+
120125
def assertRaisesMessage(self, exc_type, message,
121126
callable, *args, **kwargs):
122127
"""Asserts that callable(*args, **kwargs) raises exc_type(message)."""
@@ -349,7 +354,10 @@ def testArithmetic(self):
349354
self.assertEqual(F(1, 4), F(1, 10) / F(2, 5))
350355
self.assertTypedEquals(2, F(9, 10) // F(2, 5))
351356
self.assertTypedEquals(10**23, F(10**23, 1) // F(1))
357+
self.assertEqual(F(5, 6), F(7, 3) % F(3, 2))
352358
self.assertEqual(F(2, 3), F(-7, 3) % F(3, 2))
359+
self.assertEqual((F(1), F(5, 6)), divmod(F(7, 3), F(3, 2)))
360+
self.assertEqual((F(-2), F(2, 3)), divmod(F(-7, 3), F(3, 2)))
353361
self.assertEqual(F(8, 27), F(2, 3) ** F(3))
354362
self.assertEqual(F(27, 8), F(2, 3) ** F(-3))
355363
self.assertTypedEquals(2.0, F(4) ** F(1, 2))
@@ -371,6 +379,40 @@ def testArithmetic(self):
371379
self.assertEqual(p.numerator, 4)
372380
self.assertEqual(p.denominator, 1)
373381

382+
def testLargeArithmetic(self):
383+
self.assertTypedEquals(
384+
F(10101010100808080808080808101010101010000000000000000,
385+
1010101010101010101010101011111111101010101010101010101010101),
386+
F(10**35+1, 10**27+1) % F(10**27+1, 10**35-1)
387+
)
388+
self.assertTypedEquals(
389+
F(7, 1901475900342344102245054808064),
390+
F(-2**100, 3) % F(5, 2**100)
391+
)
392+
self.assertTypedTupleEquals(
393+
(9999999999999999,
394+
F(10101010100808080808080808101010101010000000000000000,
395+
1010101010101010101010101011111111101010101010101010101010101)),
396+
divmod(F(10**35+1, 10**27+1), F(10**27+1, 10**35-1))
397+
)
398+
self.assertTypedEquals(
399+
-2 ** 200 // 15,
400+
F(-2**100, 3) // F(5, 2**100)
401+
)
402+
self.assertTypedEquals(
403+
1,
404+
F(5, 2**100) // F(3, 2**100)
405+
)
406+
self.assertTypedEquals(
407+
(1, F(2, 2**100)),
408+
divmod(F(5, 2**100), F(3, 2**100))
409+
)
410+
self.assertTypedTupleEquals(
411+
(-2 ** 200 // 15,
412+
F(7, 1901475900342344102245054808064)),
413+
divmod(F(-2**100, 3), F(5, 2**100))
414+
)
415+
374416
def testMixedArithmetic(self):
375417
self.assertTypedEquals(F(11, 10), F(1, 10) + 1)
376418
self.assertTypedEquals(1.1, F(1, 10) + 1.0)
@@ -415,7 +457,14 @@ def testMixedArithmetic(self):
415457
self.assertTypedEquals(float('inf'), F(-1, 10) % float('inf'))
416458
self.assertTypedEquals(-0.1, F(-1, 10) % float('-inf'))
417459

418-
# No need for divmod since we don't override it.
460+
self.assertTypedTupleEquals((0, F(1, 10)), divmod(F(1, 10), 1))
461+
self.assertTypedTupleEquals(divmod(0.1, 1.0), divmod(F(1, 10), 1.0))
462+
self.assertTypedTupleEquals((10, F(0)), divmod(1, F(1, 10)))
463+
self.assertTypedTupleEquals(divmod(1.0, 0.1), divmod(1.0, F(1, 10)))
464+
self.assertTypedTupleEquals(divmod(0.1, float('inf')), divmod(F(1, 10), float('inf')))
465+
self.assertTypedTupleEquals(divmod(0.1, float('-inf')), divmod(F(1, 10), float('-inf')))
466+
self.assertTypedTupleEquals(divmod(-0.1, float('inf')), divmod(F(-1, 10), float('inf')))
467+
self.assertTypedTupleEquals(divmod(-0.1, float('-inf')), divmod(F(-1, 10), float('-inf')))
419468

420469
# ** has more interesting conversion rules.
421470
self.assertTypedEquals(F(100, 1), F(1, 10) ** -2)
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
The floor division and modulo operations and the :func:`divmod` function on :class:`fractions.Fraction` types are 2--4x faster.
2+
Patch by Stefan Behnel.

0 commit comments

Comments
 (0)