Add a minor `Fraction.__hash__()` optimization (GH-15313)
authorTim Peters <tim.peters@gmail.com>
Sat, 17 Aug 2019 02:09:16 +0000 (21:09 -0500)
committerGitHub <noreply@github.com>
Sat, 17 Aug 2019 02:09:16 +0000 (21:09 -0500)
* Add a minor `Fraction.__hash__` optimization that got lost in the shuffle.

Document the optimizations.

Lib/fractions.py

index c922c38e244116f6c0abd49162625dfdc1630a70..2e7047a81844d2465d7eecbe5638ba87d17715ed 100644 (file)
@@ -564,10 +564,25 @@ class Fraction(numbers.Rational):
         try:
             dinv = pow(self._denominator, -1, _PyHASH_MODULUS)
         except ValueError:
-            # ValueError means there is no modular inverse
+            # ValueError means there is no modular inverse.
             hash_ = _PyHASH_INF
         else:
-            hash_ = hash(abs(self._numerator)) * dinv % _PyHASH_MODULUS
+            # The general algorithm now specifies that the absolute value of
+            # the hash is
+            #    (|N| * dinv) % P
+            # where N is self._numerator and P is _PyHASH_MODULUS.  That's
+            # optimized here in two ways:  first, for a non-negative int i,
+            # hash(i) == i % P, but the int hash implementation doesn't need
+            # to divide, and is faster than doing % P explicitly.  So we do
+            #    hash(|N| * dinv)
+            # instead.  Second, N is unbounded, so its product with dinv may
+            # be arbitrarily expensive to compute.  The final answer is the
+            # same if we use the bounded |N| % P instead, which can again
+            # be done with an int hash() call.  If 0 <= i < P, hash(i) == i,
+            # so this nested hash() call wastes a bit of time making a
+            # redundant copy when |N| < P, but can save an arbitrarily large
+            # amount of computation for large |N|.
+            hash_ = hash(hash(abs(self._numerator)) * dinv)
         result = hash_ if self._numerator >= 0 else -hash_
         return -2 if result == -1 else result