-
-
Notifications
You must be signed in to change notification settings - Fork 32.7k
gh-136599: Improve long_hash #136600
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
base: main
Are you sure you want to change the base?
gh-136599: Improve long_hash #136600
Conversation
Lib/test/test_long.py
Outdated
@@ -1693,5 +1693,11 @@ class MyInt(int): | |||
# GH-117195 -- This shouldn't crash | |||
object.__sizeof__(1) | |||
|
|||
def test_long_hash(self): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1 on introducing hash tests in test_long.py
even though there are
cpython/Lib/test/test_builtin.py
Line 1165 in 3dbe02c
def test_hash(self): |
and
Lib/test/test_hash.py
There are special tests with respect to hashing in many other test modules, e.g.
cpython/Lib/test/test_float.py
Line 635 in 9e5cebd
def test_hash(self): |
so IMHO this is a good fit 👍
This may be not so on a build with 15-bit digits. In general, this should be conditional, depending on the size of the compact representation and the size of the digit, at compile time. |
Co-authored-by: Sergey B Kirpichev <skirpichev@gmail.com>
|
||
// unroll first two digits | ||
#if ( PyHASH_BITS > PyLong_SHIFT ) | ||
assert(i>=2); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe just use assert(i>=1)
here and then again in the second #if
below?
Otherwise LGTM.
self.assertEqual(hash(-sys.hash_info.modulus - 2), -2) | ||
self.assertEqual(hash(-sys.hash_info.modulus - 1), -2) | ||
self.assertEqual(hash(-sys.hash_info.modulus), 0) | ||
self.assertEqual(hash(-sys.hash_info.modulus + 1), - (sys.hash_info.modulus - 1)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
self.assertEqual(hash(-sys.hash_info.modulus + 1), - (sys.hash_info.modulus - 1)) | |
self.assertEqual(hash(-sys.hash_info.modulus + 1), -sys.hash_info.modulus + 1) |
@@ -1693,5 +1693,22 @@ class MyInt(int): | |||
# GH-117195 -- This shouldn't crash | |||
object.__sizeof__(1) | |||
|
|||
def test_hash(self): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that part could be in a separate pr, merged first.
The docs describe algorithm in details:
https://docs.python.org/3/library/stdtypes.html#hashing-of-numeric-types
Maybe we could test it against pure-Python implementation, using also hypothesis?
x = 0; | ||
|
||
// unroll first two digits | ||
#if ( PyHASH_BITS > PyLong_SHIFT ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
PyHASH_BITS can be either 31 or 61, while PyLong_SHIFT - either 15 or 30. So, this condition is always true.
BTW, it seems the 15-bit digit is untested by regular CI. Is there at least some buildbot with such settings? I'll open an issue.
#if ( PyHASH_BITS > PyLong_SHIFT ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The case is indeed untested, but I added it because of a comment by Serhiy #136600 (comment)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The case is indeed untested
FYI: #138336
I added it because of a comment by Serhiy
Then fine. Though, an assert might be option.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think long_hash() change is correct, modulo few nitpicks.
I also confirm small performance boost with OP benchmark:
Benchmark | ref | patch |
---|---|---|
long_hash_small_int | 81.5 us | 82.2 us: 1.01x slower |
long_hash_one_digit | 136 us | 137 us: 1.00x slower |
long_hash_two_digit | 186 us | 179 us: 1.04x faster |
long_hash_multi_digit | 338 us | 320 us: 1.06x faster |
set(ints) | 132 us | 120 us: 1.10x faster |
Geometric mean | (ref) | 1.04x faster |
raw data
$ python a.py -q -o ref.json
long_hash_small_int: Mean +- std dev: 81.5 us +- 0.2 us
long_hash_one_digit: Mean +- std dev: 136 us +- 2 us
long_hash_two_digit: Mean +- std dev: 186 us +- 1 us
long_hash_multi_digit: Mean +- std dev: 338 us +- 7 us
set(ints): Mean +- std dev: 132 us +- 3 us
$ python a.py -q -o patch.json
long_hash_small_int: Mean +- std dev: 82.2 us +- 0.1 us
long_hash_one_digit: Mean +- std dev: 137 us +- 1 us
long_hash_two_digit: Mean +- std dev: 179 us +- 2 us
long_hash_multi_digit: Mean +- std dev: 320 us +- 5 us
set(ints): Mean +- std dev: 120 us +- 1 us
I'm +1 for test_hash() and think that this deserve a separate pr.
I'm unsure about the rest. Some performance improvement (up to 10%) come with much less readable code. Maybe you can reorganize one in some way? I.e. the huge comment, currently in the loop - will come first to explain things, than code to unroll one-two steps, than the loop. Patch size will be much bigger, of course. In same shoot you can choose a consistent naming for PyHASH_BITS and friends (either use private or public macro).
Co-authored-by: Sergey B Kirpichev <skirpichev@gmail.com>
long_hash
: the final checklong_hash
by unrolling the first two digitsFor reviewers: the first commit adds a test, the second and third commit unroll the hash calculation for the digits. The performance gain is not a lot, so I would be fine with reverting the last two commits. A test scripts:
On my system I get a fairly consistent performance increase of 10% for the
set(ints)
test. For the other benchmarks my system (thermal throttled laptop) is not stable enough (the benchmarks are also a bit dominated by the creation of new integers).