Skip to content

Commit

Permalink
pythongh-111545: Add PyHash_Double() function
Browse files Browse the repository at this point in the history
* Cleanup _Py_HashDouble() implementation.
* Add Lib/test/test_capi/test_hash.py.
* Add Modules/_testcapi/hash.c.
  • Loading branch information
vstinner committed Nov 15, 2023
1 parent 62802b6 commit a8c4cb2
Show file tree
Hide file tree
Showing 13 changed files with 144 additions and 19 deletions.
12 changes: 12 additions & 0 deletions Doc/c-api/hash.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
.. highlight:: c

PyHash API
----------

See also the :c:member:`PyTypeObject.tp_hash` member.

.. c:function:: Py_hash_t PyHash_Double(double value)
Hash a C double number.
Return ``-1`` if *value* is not-a-number (NaN).
1 change: 1 addition & 0 deletions Doc/c-api/utilities.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ and parsing function arguments and constructing Python values from C values.
marshal.rst
arg.rst
conversion.rst
hash.rst
reflection.rst
codec.rst
perfmaps.rst
3 changes: 3 additions & 0 deletions Doc/whatsnew/3.13.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1181,6 +1181,9 @@ New Features
:exc:`KeyError` if the key missing.
(Contributed by Stefan Behnel and Victor Stinner in :gh:`111262`.)

* Add :c:func:`PyHash_Double` function to hash a C double number.
(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 @@ -11,3 +11,5 @@ typedef struct {
} PyHash_FuncDef;

PyAPI_FUNC(PyHash_FuncDef*) PyHash_GetFuncDef(void);

PyAPI_FUNC(Py_hash_t) PyHash_Double(double value);
50 changes: 50 additions & 0 deletions Lib/test/test_capi/test_hash.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import math
import sys
import unittest
from test.support import import_helper
_testcapi = import_helper.import_module('_testcapi')


PyHASH_INF = 314159
if _testcapi.SIZEOF_VOID_P >= 8:
PyHASH_BITS = 61
else:
PyHASH_BITS = 31
PyHASH_MODULUS = ((1 << PyHASH_BITS) - 1)


class CAPITest(unittest.TestCase):
def test_hash_double(self):
# Test PyHash_Double()
hash_double = _testcapi.hash_double

# test integers
integers = [*range(0, 10), 2**30 - 1, 2 ** 233]
integers.extend([-x for x in integers])
for x in integers:
expected = abs(x) % PyHASH_MODULUS
if x < 0:
expected = -expected
if expected == -1:
expected = -2
self.assertEqual(hash_double(float(x)), expected, x)

# test non-finite values
self.assertEqual(hash_double(float('inf')), PyHASH_INF)
self.assertEqual(hash_double(float('-inf')), -PyHASH_INF)
self.assertEqual(hash_double(float('nan')), -1)

# special values: compare with Python hash() function
def python_hash_double(x):
return hash(x)

special_values = (
sys.float_info.max,
sys.float_info.min,
sys.float_info.epsilon,
math.nextafter(0.0, 1.0),
)
for x in special_values:
with self.subTest(x=x):
self.assertEqual(hash_double(x), python_hash_double(x))
self.assertEqual(hash_double(-x), python_hash_double(-x))
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Add :c:func:`PyHash_Double` function to hash a C double number. Patch by
Victor Stinner.
2 changes: 1 addition & 1 deletion Modules/Setup.stdlib.in
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@
@MODULE__XXTESTFUZZ_TRUE@_xxtestfuzz _xxtestfuzz/_xxtestfuzz.c _xxtestfuzz/fuzzer.c
@MODULE__TESTBUFFER_TRUE@_testbuffer _testbuffer.c
@MODULE__TESTINTERNALCAPI_TRUE@_testinternalcapi _testinternalcapi.c _testinternalcapi/test_lock.c _testinternalcapi/pytime.c _testinternalcapi/set.c _testinternalcapi/test_critical_sections.c
@MODULE__TESTCAPI_TRUE@_testcapi _testcapimodule.c _testcapi/vectorcall.c _testcapi/vectorcall_limited.c _testcapi/heaptype.c _testcapi/abstract.c _testcapi/bytearray.c _testcapi/bytes.c _testcapi/unicode.c _testcapi/dict.c _testcapi/set.c _testcapi/list.c _testcapi/tuple.c _testcapi/getargs.c _testcapi/datetime.c _testcapi/docstring.c _testcapi/mem.c _testcapi/watchers.c _testcapi/long.c _testcapi/float.c _testcapi/complex.c _testcapi/numbers.c _testcapi/structmember.c _testcapi/exceptions.c _testcapi/code.c _testcapi/buffer.c _testcapi/pyatomic.c _testcapi/pyos.c _testcapi/file.c _testcapi/codec.c _testcapi/immortal.c _testcapi/heaptype_relative.c _testcapi/gc.c _testcapi/sys.c
@MODULE__TESTCAPI_TRUE@_testcapi _testcapimodule.c _testcapi/vectorcall.c _testcapi/vectorcall_limited.c _testcapi/heaptype.c _testcapi/abstract.c _testcapi/bytearray.c _testcapi/bytes.c _testcapi/unicode.c _testcapi/dict.c _testcapi/set.c _testcapi/list.c _testcapi/tuple.c _testcapi/getargs.c _testcapi/datetime.c _testcapi/docstring.c _testcapi/mem.c _testcapi/watchers.c _testcapi/long.c _testcapi/float.c _testcapi/complex.c _testcapi/numbers.c _testcapi/structmember.c _testcapi/exceptions.c _testcapi/code.c _testcapi/buffer.c _testcapi/pyatomic.c _testcapi/pyos.c _testcapi/file.c _testcapi/codec.c _testcapi/immortal.c _testcapi/heaptype_relative.c _testcapi/gc.c _testcapi/sys.c _testcapi/hash.c
@MODULE__TESTCLINIC_TRUE@_testclinic _testclinic.c
@MODULE__TESTCLINIC_LIMITED_TRUE@_testclinic_limited _testclinic_limited.c

Expand Down
25 changes: 25 additions & 0 deletions Modules/_testcapi/hash.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#include "parts.h"
#include "util.h"

static PyObject *
hash_double(PyObject *Py_UNUSED(module), PyObject *args)
{
double value;
if (!PyArg_ParseTuple(args, "d", &value)) {
return NULL;
}
Py_hash_t hash = PyHash_Double(value);
Py_BUILD_ASSERT(sizeof(long long) >= sizeof(hash));
return PyLong_FromLongLong(hash);
}

static PyMethodDef test_methods[] = {
{"hash_double", hash_double, METH_VARARGS},
{NULL},
};

int
_PyTestCapi_Init_Hash(PyObject *m)
{
return PyModule_AddFunctions(m, test_methods);
}
1 change: 1 addition & 0 deletions Modules/_testcapi/parts.h
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ int _PyTestCapi_Init_Codec(PyObject *module);
int _PyTestCapi_Init_Immortal(PyObject *module);
int _PyTestCapi_Init_GC(PyObject *module);
int _PyTestCapi_Init_Sys(PyObject *module);
int _PyTestCapi_Init_Hash(PyObject *module);

int _PyTestCapi_Init_VectorcallLimited(PyObject *module);
int _PyTestCapi_Init_HeaptypeRelative(PyObject *module);
Expand Down
3 changes: 3 additions & 0 deletions Modules/_testcapimodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -3995,6 +3995,9 @@ PyInit__testcapi(void)
if (_PyTestCapi_Init_HeaptypeRelative(m) < 0) {
return NULL;
}
if (_PyTestCapi_Init_Hash(m) < 0) {
return NULL;
}

PyState_AddModule(m, &_testcapimodule);
return m;
Expand Down
1 change: 1 addition & 0 deletions PCbuild/_testcapi.vcxproj
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@
<ClCompile Include="..\Modules\_testcapi\file.c" />
<ClCompile Include="..\Modules\_testcapi\codec.c" />
<ClCompile Include="..\Modules\_testcapi\sys.c" />
<ClCompile Include="..\Modules\_testcapi\hash.c" />
<ClCompile Include="..\Modules\_testcapi\immortal.c" />
<ClCompile Include="..\Modules\_testcapi\gc.c" />
</ItemGroup>
Expand Down
3 changes: 3 additions & 0 deletions PCbuild/_testcapi.vcxproj.filters
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,9 @@
<ClCompile Include="..\Modules\_testcapi\sys.c">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\Modules\_testcapi\hash.c">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\Modules\_testcapi\gc.c">
<Filter>Source Files</Filter>
</ClCompile>
Expand Down
58 changes: 40 additions & 18 deletions Python/pyhash.c
Original file line number Diff line number Diff line change
Expand Up @@ -86,49 +86,71 @@ 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)
PyHash_Double(double v)
{
int e, sign;
double m;
Py_uhash_t x, y;

if (!Py_IS_FINITE(v)) {
if (Py_IS_INFINITY(v))
if (Py_IS_INFINITY(v)) {
return v > 0 ? _PyHASH_INF : -_PyHASH_INF;
else
return _Py_HashPointer(inst);
}
else {
assert(Py_IS_NAN(v));
return -1;
}
}

m = frexp(v, &e);

sign = 1;
int e;
double m = frexp(v, &e);
int sign;
if (m < 0) {
sign = -1;
m = -m;
}
else {
sign = 1;
}

/* process 28 bits at a time; this should work well both for binary
and hexadecimal floating point. */
x = 0;
Py_uhash_t x = 0;
while (m) {
x = ((x << 28) & _PyHASH_MODULUS) | x >> (_PyHASH_BITS - 28);
m *= 268435456.0; /* 2**28 */
e -= 28;
y = (Py_uhash_t)m; /* pull out integer part */

Py_uhash_t y = (Py_uhash_t)m; /* pull out integer part */
m -= y;
x += y;
if (x >= _PyHASH_MODULUS)
if (x >= _PyHASH_MODULUS) {
x -= _PyHASH_MODULUS;
}
}

/* adjust for the exponent; first reduce it modulo _PyHASH_BITS */
e = e >= 0 ? e % _PyHASH_BITS : _PyHASH_BITS-1-((-1-e) % _PyHASH_BITS);
if (e >= 0) {
e = e % _PyHASH_BITS;
}
else {
e = _PyHASH_BITS - 1 - ((-1 - e) % _PyHASH_BITS);
}
x = ((x << e) & _PyHASH_MODULUS) | x >> (_PyHASH_BITS - e);

x = x * sign;
if (x == (Py_uhash_t)-1)
x = (Py_uhash_t)-2;
return (Py_hash_t)x;

Py_hash_t result = (Py_hash_t)x;
if (result == -1) {
result = -2;
}
return (Py_hash_t)result;
}

Py_hash_t
_Py_HashDouble(PyObject *inst, double v)
{
Py_hash_t hash = PyHash_Double(v);
if (hash == -1) {
return _Py_HashPointer(inst);
}
return hash;
}

Py_hash_t
Expand Down

0 comments on commit a8c4cb2

Please sign in to comment.