Skip to content

Commit 992c39e

Browse files
committed
pythongh-111545: Add PyHash_Pointer() function
* Keep _Py_HashPointer() function as an alias to PyHash_Pointer(). * Add PyHash_Pointer() tests to test_capi.test_hash. * Remove _Py_HashPointerRaw() function: inline code in _Py_hashtable_hash_ptr().
1 parent d4f83e1 commit 992c39e

File tree

10 files changed

+109
-23
lines changed

10 files changed

+109
-23
lines changed

Doc/c-api/hash.rst

+9
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,12 @@ See also the :c:member:`PyTypeObject.tp_hash` member.
4646
Get the hash function definition.
4747
4848
.. versionadded:: 3.4
49+
50+
51+
.. c:function:: Py_hash_t PyHash_Pointer(const void *ptr)
52+
53+
Hash a pointer.
54+
55+
The function cannot fail: it cannot return ``-1``.
56+
57+
.. versionadded:: 3.13

Doc/whatsnew/3.13.rst

+3
Original file line numberDiff line numberDiff line change
@@ -1181,6 +1181,9 @@ New Features
11811181
:exc:`KeyError` if the key missing.
11821182
(Contributed by Stefan Behnel and Victor Stinner in :gh:`111262`.)
11831183

1184+
* Add :c:func:`PyHash_Pointer` function to hash a pointer.
1185+
(Contributed by Victor Stinner in :gh:`111545`.)
1186+
11841187

11851188
Porting to Python 3.13
11861189
----------------------

Include/cpython/pyhash.h

+2
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,5 @@ typedef struct {
1111
} PyHash_FuncDef;
1212

1313
PyAPI_FUNC(PyHash_FuncDef*) PyHash_GetFuncDef(void);
14+
15+
PyAPI_FUNC(Py_hash_t) PyHash_Pointer(const void *ptr);

Include/internal/pycore_pyhash.h

+2-5
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,8 @@
88
/* Helpers for hash functions */
99
extern Py_hash_t _Py_HashDouble(PyObject *, double);
1010

11-
// Export for '_decimal' shared extension
12-
PyAPI_FUNC(Py_hash_t) _Py_HashPointer(const void*);
13-
14-
// Similar to _Py_HashPointer(), but don't replace -1 with -2
15-
extern Py_hash_t _Py_HashPointerRaw(const void*);
11+
// Kept for backward compatibility
12+
#define _Py_HashPointer PyHash_Pointer
1613

1714
// Export for '_datetime' shared extension
1815
PyAPI_FUNC(Py_hash_t) _Py_HashBytes(const void*, Py_ssize_t);

Lib/test/test_capi/test_hash.py

+47-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
_testcapi = import_helper.import_module('_testcapi')
55

66

7-
SIZEOF_PY_HASH_T = _testcapi.SIZEOF_VOID_P
7+
SIZEOF_VOID_P = _testcapi.SIZEOF_VOID_P
8+
SIZEOF_PY_HASH_T = SIZEOF_VOID_P
89

910

1011
class CAPITest(unittest.TestCase):
@@ -31,3 +32,48 @@ def test_hash_getfuncdef(self):
3132
self.assertEqual(func_def.name, hash_info.algorithm)
3233
self.assertEqual(func_def.hash_bits, hash_info.hash_bits)
3334
self.assertEqual(func_def.seed_bits, hash_info.seed_bits)
35+
36+
def test_hash_pointer(self):
37+
# Test PyHash_Pointer()
38+
hash_pointer = _testcapi.hash_pointer
39+
40+
UHASH_T_MASK = ((2 ** (8 * SIZEOF_PY_HASH_T)) - 1)
41+
HASH_T_MAX = (2 ** (8 * SIZEOF_PY_HASH_T - 1) - 1)
42+
43+
def python_hash_pointer(x):
44+
# PyHash_Pointer() rotates the pointer bits by 4 bits to the right
45+
x = (x >> 4) | ((x & 15) << (8 * SIZEOF_VOID_P - 4))
46+
47+
# Convert unsigned uintptr_t to signed Py_hash_t
48+
if HASH_T_MAX < x:
49+
x = (~x) + 1
50+
x &= UHASH_T_MASK
51+
x = (~x) + 1
52+
return x
53+
54+
if SIZEOF_VOID_P == 8:
55+
values = (
56+
0xABCDEF1234567890,
57+
0x1234567890ABCDEF,
58+
0xFEE4ABEDD1CECA5E,
59+
)
60+
else:
61+
values = (
62+
0x12345678,
63+
0x1234ABCD,
64+
0xDEADCAFE,
65+
)
66+
67+
for value in values:
68+
expected = python_hash_pointer(value)
69+
with self.subTest(value=value):
70+
self.assertEqual(hash_pointer(value), expected,
71+
f"hash_pointer({value:x}) = "
72+
f"{hash_pointer(value):x} != {expected:x}")
73+
74+
# PyHash_Pointer(NULL) returns 0
75+
self.assertEqual(hash_pointer(0), 0)
76+
77+
# PyHash_Pointer((void*)(uintptr_t)-1) doesn't return -1 but -2
78+
VOID_P_MAX = -1 & (2 ** (8 * SIZEOF_VOID_P) - 1)
79+
self.assertEqual(hash_pointer(VOID_P_MAX), -2)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Add :c:func:`PyHash_Pointer` function to hash a pointer. Patch by Victor
2+
Stinner.

Modules/_testcapi/hash.c

+16
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,24 @@ hash_getfuncdef(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args))
4444
return result;
4545
}
4646

47+
48+
static PyObject *
49+
hash_pointer(PyObject *Py_UNUSED(module), PyObject *arg)
50+
{
51+
void *ptr = PyLong_AsVoidPtr(arg);
52+
if (ptr == NULL && PyErr_Occurred()) {
53+
return NULL;
54+
}
55+
56+
Py_hash_t hash = PyHash_Pointer(ptr);
57+
Py_BUILD_ASSERT(sizeof(long long) >= sizeof(hash));
58+
return PyLong_FromLongLong(hash);
59+
}
60+
61+
4762
static PyMethodDef test_methods[] = {
4863
{"hash_getfuncdef", hash_getfuncdef, METH_NOARGS},
64+
{"hash_pointer", hash_pointer, METH_O},
4965
{NULL},
5066
};
5167

PC/pyconfig.h

+2
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,8 @@ Py_NO_ENABLE_SHARED to find out. Also support MS_NO_COREDLL for b/w compat */
355355
# define ALIGNOF_MAX_ALIGN_T 8
356356
#endif
357357

358+
#define SIZEOF_UINTPTR_T SIZEOF_VOID_P
359+
358360
#ifdef _DEBUG
359361
# define Py_DEBUG
360362
#endif

Python/hashtable.c

+13-3
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,7 @@
4545
*/
4646

4747
#include "Python.h"
48-
#include "pycore_hashtable.h"
49-
#include "pycore_pyhash.h" // _Py_HashPointerRaw()
48+
#include "pycore_hashtable.h" // export _Py_hashtable_new()
5049

5150
#define HASHTABLE_MIN_SIZE 16
5251
#define HASHTABLE_HIGH 0.50
@@ -89,10 +88,21 @@ _Py_slist_remove(_Py_slist_t *list, _Py_slist_item_t *previous,
8988
}
9089

9190

91+
// Similar to PyHash_Pointer() but avoid "if (x == -1) x = -2;" for best
92+
// performance. The value (Py_uhash_t)-1 is not special for
93+
// _Py_hashtable_t.hash_func function, there is no need to replace it with -2.
9294
Py_uhash_t
9395
_Py_hashtable_hash_ptr(const void *key)
9496
{
95-
return (Py_uhash_t)_Py_HashPointerRaw(key);
97+
uintptr_t x = (uintptr_t)key;
98+
Py_BUILD_ASSERT(sizeof(x) == sizeof(key));
99+
100+
// Bottom 3 or 4 bits are likely to be 0; rotate x by 4 to the right
101+
// to avoid excessive hash collisions.
102+
x = (x >> 4) | (x << (8 * SIZEOF_UINTPTR_T - 4));
103+
104+
Py_BUILD_ASSERT(sizeof(x) == sizeof(Py_hash_t));
105+
return (Py_uhash_t)x;
96106
}
97107

98108

Python/pyhash.c

+13-14
Original file line numberDiff line numberDiff line change
@@ -131,24 +131,23 @@ _Py_HashDouble(PyObject *inst, double v)
131131
return (Py_hash_t)x;
132132
}
133133

134+
// See also _Py_hashtable_hash_ptr() which is similar but can return -1.
134135
Py_hash_t
135-
_Py_HashPointerRaw(const void *p)
136+
PyHash_Pointer(const void *ptr)
136137
{
137-
size_t y = (size_t)p;
138-
/* bottom 3 or 4 bits are likely to be 0; rotate y by 4 to avoid
139-
excessive hash collisions for dicts and sets */
140-
y = (y >> 4) | (y << (8 * SIZEOF_VOID_P - 4));
141-
return (Py_hash_t)y;
142-
}
138+
uintptr_t x = (uintptr_t)ptr;
139+
Py_BUILD_ASSERT(sizeof(x) == sizeof(ptr));
143140

144-
Py_hash_t
145-
_Py_HashPointer(const void *p)
146-
{
147-
Py_hash_t x = _Py_HashPointerRaw(p);
148-
if (x == -1) {
149-
x = -2;
141+
// Bottom 3 or 4 bits are likely to be 0; rotate x by 4 to the right
142+
// to avoid excessive hash collisions for dicts and sets.
143+
x = (x >> 4) | (x << (8 * SIZEOF_UINTPTR_T - 4));
144+
145+
Py_BUILD_ASSERT(sizeof(x) == sizeof(Py_hash_t));
146+
Py_hash_t result = (Py_hash_t)x;
147+
if (result == -1) {
148+
result = -2;
150149
}
151-
return x;
150+
return result;
152151
}
153152

154153
Py_hash_t

0 commit comments

Comments
 (0)