-
-
Notifications
You must be signed in to change notification settings - Fork 32.4k
bpo-43420: Simple optimizations for Fraction's arithmetics #24779
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
598b3c9
430e476
046c84e
772bec6
3d5d321
32778ab
ea38398
f4b151a
40401af
020037a
bde52d1
51b52b6
2789350
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -445,6 +445,10 @@ def reverse(b, a): | |
# Indeed, pick (na//g1). It's coprime with (da//g2), because input | ||
# fractions are normalized. It's also coprime with (db//g1), because | ||
# common factors are removed by g1 == gcd(na, db). | ||
# | ||
# As for addition/substraction, we should special-case g1 == 1 | ||
# and g2 == 1 for same reason. That happens also for multiplying | ||
# rationals, obtained from floats. | ||
|
||
def _add_sub_(a, b, pm=int.__add__): | ||
na, da = a._numerator, a._denominator | ||
|
@@ -474,9 +478,14 @@ def _mul(a, b): | |
na, da = a._numerator, a._denominator | ||
nb, db = b._numerator, b._denominator | ||
g1 = math.gcd(na, db) | ||
if g1 > 1: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the "> 1" tests should be removed. The cost of a test is in the same ballpark as the cost of a division. The code is clearer if you just always divide. The performance gain mostly comes from two small gcd's being faster than a single big gcd. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Maybe, I didn't benchmark this closely, but...
This is a micro-optimization, yes. But ">" has not same cost as "//" even in the CPython:
Maybe my measurements are primitive, but there is some pattern...
I tried to explain reasons for branching in the comment above code. Careful reader might ask: why you don't include same optimizations in the On another hand, gmplib doesn't use thise optimizations (c.f. same in mpz_add/sub). SymPy's PythonRational class - too. So, I did my best in defending those branches and I'm fine with removing, if @tim-one has no arguments against.
This is more important, yes. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
? Comparing There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't mind either way. If it were just me, I would leave out the '> 1' tests because:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That just doesn't make much sense to me. On my box just now, with the released 3.9.1:
So doing the useless division not only drove the per-iteration measure from 24.2 to 32.4, the unit increased by a factor of 1000(!), from nanoseconds to microseconds. That's using, by construction, an exactly 100,001 bit numerator. And, no, removing the test barely improves that relative disaster at all:
BTW, as expected, boost the number of numerator bits by a factor of 10 (to a million+1), and per-loop expense also becomes 10 times as much; cut it by a factor 10, and similarly the per-loop expense is also cut by a factor of 10. With respect to the BPO message you linked to, it appeared atypical: both gcds were greater than 1: |
||
na //= g1 | ||
skirpichev marked this conversation as resolved.
Show resolved
Hide resolved
|
||
db //= g1 | ||
g2 = math.gcd(nb, da) | ||
return Fraction((na // g1) * (nb // g2), | ||
(db // g1) * (da // g2), _normalize=False) | ||
if g2 > 1: | ||
nb //= g2 | ||
da //= g2 | ||
return Fraction(na * nb, db * da, _normalize=False) | ||
|
||
__mul__, __rmul__ = _operator_fallbacks(_mul, operator.mul) | ||
|
||
|
@@ -486,8 +495,14 @@ def _div(a, b): | |
na, da = a._numerator, a._denominator | ||
nb, db = b._numerator, b._denominator | ||
g1 = math.gcd(na, nb) | ||
if g1 > 1: | ||
na //= g1 | ||
nb //= g1 | ||
g2 = math.gcd(db, da) | ||
n, d = (na // g1) * (db // g2), (nb // g1) * (da // g2) | ||
if g2 > 1: | ||
da //= g2 | ||
db //= g2 | ||
n, d = na * db, nb * da | ||
if nb < 0: | ||
skirpichev marked this conversation as resolved.
Show resolved
Hide resolved
|
||
n, d = -n, -d | ||
skirpichev marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return Fraction(n, d, _normalize=False) | ||
|
Uh oh!
There was an error while loading. Please reload this page.