Skip to content

Commit

Permalink
gh-111545: Add Py_HashPointer() function
Browse files Browse the repository at this point in the history
* Implement _Py_HashPointerRaw() as a static inline function.
* Add Py_HashPointer() tests to test_capi.test_hash.
* Keep _Py_HashPointer() function as an alias to Py_HashPointer().
  • Loading branch information
vstinner committed Dec 1, 2023
1 parent 05a370a commit 8031210
Show file tree
Hide file tree
Showing 9 changed files with 103 additions and 21 deletions.
9 changes: 9 additions & 0 deletions Doc/c-api/hash.rst
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,12 @@ See also the :c:member:`PyTypeObject.tp_hash` member.
Get the hash function definition.
.. versionadded:: 3.4
.. c:function:: Py_hash_t Py_HashPointer(const void *ptr)
Hash a pointer.
The function cannot fail: it cannot return ``-1``.
.. versionadded:: 3.13
3 changes: 3 additions & 0 deletions Doc/whatsnew/3.13.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1230,6 +1230,9 @@ New Features
:exc:`KeyError` if the key missing.
(Contributed by Stefan Behnel and Victor Stinner in :gh:`111262`.)

* Add :c:func:`Py_HashPointer` function to hash a pointer.
(Contributed by Victor Stinner in :gh:`111545`.)


Porting to Python 3.13
----------------------
Expand Down
2 changes: 2 additions & 0 deletions Include/cpython/pyhash.h
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,5 @@ typedef struct {
} PyHash_FuncDef;

PyAPI_FUNC(PyHash_FuncDef*) PyHash_GetFuncDef(void);

PyAPI_FUNC(Py_hash_t) Py_HashPointer(const void *ptr);
20 changes: 18 additions & 2 deletions Include/internal/pycore_pyhash.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,24 @@
# error "this header requires Py_BUILD_CORE define"
#endif

// Similar to _Py_HashPointer(), but don't replace -1 with -2
extern Py_hash_t _Py_HashPointerRaw(const void*);
// Similar to Py_HashPointer(), but don't replace -1 with -2.
static inline Py_hash_t
_Py_HashPointerRaw(const void *ptr)
{
uintptr_t x = (uintptr_t)ptr;
Py_BUILD_ASSERT(sizeof(x) == sizeof(ptr));

// Bottom 3 or 4 bits are likely to be 0; rotate x by 4 to the right
// to avoid excessive hash collisions for dicts and sets.
x = (x >> 4) | (x << (8 * sizeof(uintptr_t) - 4));

Py_BUILD_ASSERT(sizeof(x) == sizeof(Py_hash_t));
return (Py_hash_t)x;
}


// Kept for backward compatibility
#define _Py_HashPointer Py_HashPointer

// Export for '_datetime' shared extension
PyAPI_FUNC(Py_hash_t) _Py_HashBytes(const void*, Py_ssize_t);
Expand Down
48 changes: 47 additions & 1 deletion Lib/test/test_capi/test_hash.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
_testcapi = import_helper.import_module('_testcapi')


SIZEOF_PY_HASH_T = _testcapi.SIZEOF_VOID_P
SIZEOF_VOID_P = _testcapi.SIZEOF_VOID_P
SIZEOF_PY_HASH_T = SIZEOF_VOID_P


class CAPITest(unittest.TestCase):
Expand All @@ -31,3 +32,48 @@ def test_hash_getfuncdef(self):
self.assertEqual(func_def.name, hash_info.algorithm)
self.assertEqual(func_def.hash_bits, hash_info.hash_bits)
self.assertEqual(func_def.seed_bits, hash_info.seed_bits)

def test_hash_pointer(self):
# Test Py_HashPointer()
hash_pointer = _testcapi.hash_pointer

UHASH_T_MASK = ((2 ** (8 * SIZEOF_PY_HASH_T)) - 1)
HASH_T_MAX = (2 ** (8 * SIZEOF_PY_HASH_T - 1) - 1)

def python_hash_pointer(x):
# Py_HashPointer() rotates the pointer bits by 4 bits to the right
x = (x >> 4) | ((x & 15) << (8 * SIZEOF_VOID_P - 4))

# Convert unsigned uintptr_t (Py_uhash_t) to signed Py_hash_t
if HASH_T_MAX < x:
x = (~x) + 1
x &= UHASH_T_MASK
x = (~x) + 1
return x

if SIZEOF_VOID_P == 8:
values = (
0xABCDEF1234567890,
0x1234567890ABCDEF,
0xFEE4ABEDD1CECA5E,
)
else:
values = (
0x12345678,
0x1234ABCD,
0xDEADCAFE,
)

for value in values:
expected = python_hash_pointer(value)
with self.subTest(value=value):
self.assertEqual(hash_pointer(value), expected,
f"hash_pointer({value:x}) = "
f"{hash_pointer(value):x} != {expected:x}")

# Py_HashPointer(NULL) returns 0
self.assertEqual(hash_pointer(0), 0)

# Py_HashPointer((void*)(uintptr_t)-1) doesn't return -1 but -2
VOID_P_MAX = -1 & (2 ** (8 * SIZEOF_VOID_P) - 1)
self.assertEqual(hash_pointer(VOID_P_MAX), -2)
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Add :c:func:`Py_HashPointer` function to hash a pointer. Patch by Victor
Stinner.
16 changes: 16 additions & 0 deletions Modules/_testcapi/hash.c
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,24 @@ hash_getfuncdef(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args))
return result;
}


static PyObject *
hash_pointer(PyObject *Py_UNUSED(module), PyObject *arg)
{
void *ptr = PyLong_AsVoidPtr(arg);
if (ptr == NULL && PyErr_Occurred()) {
return NULL;
}

Py_hash_t hash = Py_HashPointer(ptr);
Py_BUILD_ASSERT(sizeof(long long) >= sizeof(hash));
return PyLong_FromLongLong(hash);
}


static PyMethodDef test_methods[] = {
{"hash_getfuncdef", hash_getfuncdef, METH_NOARGS},
{"hash_pointer", hash_pointer, METH_O},
{NULL},
};

Expand Down
2 changes: 1 addition & 1 deletion Python/hashtable.c
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
*/

#include "Python.h"
#include "pycore_hashtable.h"
#include "pycore_hashtable.h" // export _Py_hashtable_new()
#include "pycore_pyhash.h" // _Py_HashPointerRaw()

#define HASHTABLE_MIN_SIZE 16
Expand Down
22 changes: 5 additions & 17 deletions Python/pyhash.c
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,6 @@ static Py_ssize_t hashstats[Py_HASH_STATS_MAX + 1] = {0};
*/

Py_hash_t _Py_HashPointer(const void *);

Py_hash_t
_Py_HashDouble(PyObject *inst, double v)
{
Expand Down Expand Up @@ -132,23 +130,13 @@ _Py_HashDouble(PyObject *inst, double v)
}

Py_hash_t
_Py_HashPointerRaw(const void *p)
{
size_t y = (size_t)p;
/* bottom 3 or 4 bits are likely to be 0; rotate y by 4 to avoid
excessive hash collisions for dicts and sets */
y = (y >> 4) | (y << (8 * SIZEOF_VOID_P - 4));
return (Py_hash_t)y;
}

Py_hash_t
_Py_HashPointer(const void *p)
Py_HashPointer(const void *ptr)
{
Py_hash_t x = _Py_HashPointerRaw(p);
if (x == -1) {
x = -2;
Py_hash_t hash = _Py_HashPointerRaw(ptr);
if (hash == -1) {
hash = -2;
}
return x;
return hash;
}

Py_hash_t
Expand Down

0 comments on commit 8031210

Please sign in to comment.