hashing.py 26.6 KB
Newer Older
dugupeiwen's avatar
dugupeiwen committed
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
"""
Hash implementations for Numba types
"""

import math
import numpy as np
import sys
import ctypes
import warnings
from collections import namedtuple

import llvmlite.binding as ll
from llvmlite import ir

from numba import literal_unroll
from numba.core.extending import (
    overload, overload_method, intrinsic, register_jitable)
from numba.core import errors
from numba.core import types, utils
from numba.core.unsafe.bytes import grab_byte, grab_uint64_t
from numba.cpython.randomimpl import (const_int, get_next_int, get_next_int32,
                                      get_state_ptr)

_py310_or_later = utils.PYVERSION >= (3, 10)

# This is Py_hash_t, which is a Py_ssize_t, which has sizeof(size_t):
# https://github.com/python/cpython/blob/d1dd6be613381b996b9071443ef081de8e5f3aff/Include/pyport.h#L91-L96    # noqa: E501
_hash_width = sys.hash_info.width
_Py_hash_t = getattr(types, 'int%s' % _hash_width)
_Py_uhash_t = getattr(types, 'uint%s' % _hash_width)

# Constants from CPython source, obtained by various means:
# https://github.com/python/cpython/blob/d1dd6be613381b996b9071443ef081de8e5f3aff/Include/pyhash.h    # noqa: E501
_PyHASH_INF = sys.hash_info.inf
_PyHASH_NAN = sys.hash_info.nan
_PyHASH_MODULUS = _Py_uhash_t(sys.hash_info.modulus)
_PyHASH_BITS = 31 if types.intp.bitwidth == 32 else 61  # mersenne primes
_PyHASH_MULTIPLIER = 0xf4243  # 1000003UL
_PyHASH_IMAG = _PyHASH_MULTIPLIER
_PyLong_SHIFT = sys.int_info.bits_per_digit
_Py_HASH_CUTOFF = sys.hash_info.cutoff
_Py_hashfunc_name = sys.hash_info.algorithm


# This stub/overload pair are used to force branch pruning to remove the dead
# branch based on the potential `None` type of the hash_func which works better
# if the predicate for the prune in an ir.Arg. The obj is an arg to allow for
# a custom error message.
def _defer_hash(hash_func):
    pass


@overload(_defer_hash)
def ol_defer_hash(obj, hash_func):
    err_msg = f"unhashable type: '{obj}'"

    def impl(obj, hash_func):
        if hash_func is None:
            raise TypeError(err_msg)
        else:
            return hash_func()
    return impl


# hash(obj) is implemented by calling obj.__hash__()
@overload(hash)
def hash_overload(obj):
    attempt_generic_msg = ("No __hash__ is defined for object of type "
                           f"'{obj}' and a generic hash() cannot be "
                           "performed as there is no suitable object "
                           "represention in Numba compiled code!")

    def impl(obj):
        if hasattr(obj, '__hash__'):
            return _defer_hash(obj, getattr(obj, '__hash__'))
        else:
            raise TypeError(attempt_generic_msg)
    return impl


@register_jitable
def process_return(val):
    asint = _Py_hash_t(val)
    if (asint == int(-1)):
        asint = int(-2)
    return asint


# This is a translation of CPython's _Py_HashDouble:
# https://github.com/python/cpython/blob/d1dd6be613381b996b9071443ef081de8e5f3aff/Python/pyhash.c#L34-L129   # noqa: E501
# NOTE: In Python 3.10 hash of nan is now hash of the pointer to the PyObject
# containing said nan. Numba cannot replicate this as there is no object, so it
# elects to replicate the behaviour i.e. hash of nan is something "unique" which
# satisfies https://bugs.python.org/issue43475.

@register_jitable(locals={'x': _Py_uhash_t,
                          'y': _Py_uhash_t,
                          'm': types.double,
                          'e': types.intc,
                          'sign': types.intc,
                          '_PyHASH_MODULUS': _Py_uhash_t,
                          '_PyHASH_BITS': types.intc})
def _Py_HashDouble(v):
    if not np.isfinite(v):
        if (np.isinf(v)):
            if (v > 0):
                return _PyHASH_INF
            else:
                return -_PyHASH_INF
        else:
            # Python 3.10 does not use `_PyHASH_NAN`.
            # https://github.com/python/cpython/blob/2c4792264f9218692a1bd87398a60591f756b171/Python/pyhash.c#L102   # noqa: E501
            # Numba returns a pseudo-random number to reflect the spirit of the
            # change.
            if _py310_or_later:
                x = _prng_random_hash()
                return process_return(x)
            else:
                return _PyHASH_NAN

    m, e = math.frexp(v)

    sign = 1
    if (m < 0):
        sign = -1
        m = -m

    # process 28 bits at a time;  this should work well both for binary
    #  and hexadecimal floating point.
    x = 0
    while (m):
        x = ((x << 28) & _PyHASH_MODULUS) | x >> (_PyHASH_BITS - 28)
        m *= 268435456.0  # /* 2**28 */
        e -= 28
        y = int(m)  # /* pull out integer part */
        m -= y
        x += y
        if x >= _PyHASH_MODULUS:
            x -= _PyHASH_MODULUS
    # /* adjust for the exponent;  first reduce it modulo _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
    return process_return(x)


@intrinsic
def _fpext(tyctx, val):
    def impl(cgctx, builder, signature, args):
        val = args[0]
        return builder.fpext(val, ir.DoubleType())
    sig = types.float64(types.float32)
    return sig, impl


@intrinsic
def _prng_random_hash(tyctx):

    def impl(cgctx, builder, signature, args):
        state_ptr = get_state_ptr(cgctx, builder, "internal")
        bits = const_int(_hash_width)

        # Why not just use get_next_int() with the correct bitwidth?
        # get_next_int() always returns an i64, because the bitwidth it is
        # passed may not be a compile-time constant, so it needs to allocate
        # the largest unit of storage that may be required. Therefore, if the
        # hash width is 32, then we need to use get_next_int32() to ensure we
        # don't return a wider-than-expected hash, even if everything above
        # the low 32 bits would have been zero.
        if _hash_width == 32:
            value = get_next_int32(cgctx, builder, state_ptr)
        else:
            value = get_next_int(cgctx, builder, state_ptr, bits, False)

        return value

    sig = _Py_hash_t()
    return sig, impl


# This is a translation of CPython's long_hash, but restricted to the numerical
# domain reachable by int64/uint64 (i.e. no BigInt like support):
# https://github.com/python/cpython/blob/d1dd6be613381b996b9071443ef081de8e5f3aff/Objects/longobject.c#L2934-L2989    # noqa: E501
# obdigit is a uint32_t which is typedef'd to digit
# int32_t is typedef'd to sdigit


@register_jitable(locals={'x': _Py_uhash_t,
                          'p1': _Py_uhash_t,
                          'p2': _Py_uhash_t,
                          'p3': _Py_uhash_t,
                          'p4': _Py_uhash_t,
                          '_PyHASH_MODULUS': _Py_uhash_t,
                          '_PyHASH_BITS': types.int32,
                          '_PyLong_SHIFT': types.int32,})
def _long_impl(val):
    # This function assumes val came from a long int repr with val being a
    # uint64_t this means having to split the input into PyLong_SHIFT size
    # chunks in an unsigned hash wide type, max numba can handle is a 64bit int

    # mask to select low _PyLong_SHIFT bits
    _tmp_shift = 32 - _PyLong_SHIFT
    mask_shift = (~types.uint32(0x0)) >> _tmp_shift

    # a 64bit wide max means Numba only needs 3 x 30 bit values max,
    # or 5 x 15 bit values max on 32bit platforms
    i = (64 // _PyLong_SHIFT) + 1

    # alg as per hash_long
    x = 0
    p3 = (_PyHASH_BITS - _PyLong_SHIFT)
    for idx in range(i - 1, -1, -1):
        p1 = x << _PyLong_SHIFT
        p2 = p1 & _PyHASH_MODULUS
        p4 = x >> p3
        x = p2 | p4
        # the shift and mask splits out the `ob_digit` parts of a Long repr
        x += types.uint32((val >> idx * _PyLong_SHIFT) & mask_shift)
        if x >= _PyHASH_MODULUS:
            x -= _PyHASH_MODULUS
    return _Py_hash_t(x)


# This has no CPython equivalent, CPython uses long_hash.
@overload_method(types.Integer, '__hash__')
@overload_method(types.Boolean, '__hash__')
def int_hash(val):

    _HASH_I64_MIN = -2 if sys.maxsize <= 2 ** 32 else -4
    _SIGNED_MIN = types.int64(-0x8000000000000000)

    # Find a suitable type to hold a "big" value, i.e. iinfo(ty).min/max
    # this is to ensure e.g. int32.min is handled ok as it's abs() is its value
    _BIG = types.int64 if getattr(val, 'signed', False) else types.uint64

    # this is a bit involved due to the CPython repr of ints
    def impl(val):
        # If the magnitude is under PyHASH_MODULUS, just return the
        # value val as the hash, couple of special cases if val == val:
        # 1. it's 0, in which case return 0
        # 2. it's signed int minimum value, return the value CPython computes
        # but Numba cannot as there's no type wide enough to hold the shifts.
        #
        # If the magnitude is greater than PyHASH_MODULUS then... if the value
        # is negative then negate it switch the sign on the hash once computed
        # and use the standard wide unsigned hash implementation
        val = _BIG(val)
        mag = abs(val)
        if mag < _PyHASH_MODULUS:
            if val == 0:
                ret = 0
            elif val == _SIGNED_MIN:  # e.g. int64 min, -0x8000000000000000
                ret = _Py_hash_t(_HASH_I64_MIN)
            else:
                ret = _Py_hash_t(val)
        else:
            needs_negate = False
            if val < 0:
                val = -val
                needs_negate = True
            ret = _long_impl(val)
            if needs_negate:
                ret = -ret
        return process_return(ret)
    return impl

# This is a translation of CPython's float_hash:
# https://github.com/python/cpython/blob/d1dd6be613381b996b9071443ef081de8e5f3aff/Objects/floatobject.c#L528-L532    # noqa: E501


@overload_method(types.Float, '__hash__')
def float_hash(val):
    if val.bitwidth == 64:
        def impl(val):
            hashed = _Py_HashDouble(val)
            return hashed
    else:
        def impl(val):
            # widen the 32bit float to 64bit
            fpextended = np.float64(_fpext(val))
            hashed = _Py_HashDouble(fpextended)
            return hashed
    return impl

# This is a translation of CPython's complex_hash:
# https://github.com/python/cpython/blob/d1dd6be613381b996b9071443ef081de8e5f3aff/Objects/complexobject.c#L408-L428    # noqa: E501


@overload_method(types.Complex, '__hash__')
def complex_hash(val):
    def impl(val):
        hashreal = hash(val.real)
        hashimag = hash(val.imag)
        # Note:  if the imaginary part is 0, hashimag is 0 now,
        # so the following returns hashreal unchanged.  This is
        # important because numbers of different types that
        # compare equal must have the same hash value, so that
        # hash(x + 0*j) must equal hash(x).
        combined = hashreal + _PyHASH_IMAG * hashimag
        return process_return(combined)
    return impl


# Python 3.8 strengthened its hash alg for tuples.
# This is a translation of CPython's tuplehash for Python >=3.8
# https://github.com/python/cpython/blob/b738237d6792acba85b1f6e6c8993a812c7fd815/Objects/tupleobject.c#L338-L391    # noqa: E501

# These consts are needed for this alg variant, they are from:
# https://github.com/python/cpython/blob/b738237d6792acba85b1f6e6c8993a812c7fd815/Objects/tupleobject.c#L353-L363    # noqa: E501
if _Py_uhash_t.bitwidth // 8 > 4:
    _PyHASH_XXPRIME_1 = _Py_uhash_t(11400714785074694791)
    _PyHASH_XXPRIME_2 = _Py_uhash_t(14029467366897019727)
    _PyHASH_XXPRIME_5 = _Py_uhash_t(2870177450012600261)

    @register_jitable(locals={'x': types.uint64})
    def _PyHASH_XXROTATE(x):
        # Rotate left 31 bits
        return ((x << types.uint64(31)) | (x >> types.uint64(33)))
else:
    _PyHASH_XXPRIME_1 = _Py_uhash_t(2654435761)
    _PyHASH_XXPRIME_2 = _Py_uhash_t(2246822519)
    _PyHASH_XXPRIME_5 = _Py_uhash_t(374761393)

    @register_jitable(locals={'x': types.uint64})
    def _PyHASH_XXROTATE(x):
        # Rotate left 13 bits
        return ((x << types.uint64(13)) | (x >> types.uint64(19)))


@register_jitable(locals={'acc': _Py_uhash_t, 'lane': _Py_uhash_t,
                          '_PyHASH_XXPRIME_5': _Py_uhash_t,
                          '_PyHASH_XXPRIME_1': _Py_uhash_t,
                          'tl': _Py_uhash_t})
def _tuple_hash(tup):
    tl = len(tup)
    acc = _PyHASH_XXPRIME_5
    for x in literal_unroll(tup):
        lane = hash(x)
        if lane == _Py_uhash_t(-1):
            return -1
        acc += lane * _PyHASH_XXPRIME_2
        acc = _PyHASH_XXROTATE(acc)
        acc *= _PyHASH_XXPRIME_1

    acc += tl ^ (_PyHASH_XXPRIME_5 ^ _Py_uhash_t(3527539))

    if acc == _Py_uhash_t(-1):
        return process_return(1546275796)

    return process_return(acc)


@overload_method(types.BaseTuple, '__hash__')
def tuple_hash(val):
    def impl(val):
        return _tuple_hash(val)
    return impl


# ------------------------------------------------------------------------------
# String/bytes hashing needs hashseed info, this is from:
# https://stackoverflow.com/a/41088757
# with thanks to Martijn Pieters
#
# Developer note:
# CPython makes use of an internal "hashsecret" which is essentially a struct
# containing some state that is set on CPython initialization and contains magic
# numbers used particularly in unicode/string hashing. This code binds to the
# Python runtime libraries in use by the current process and reads the
# "hashsecret" state so that it can be used by Numba. As this is done at runtime
# the behaviour and influence of the PYTHONHASHSEED environment variable is
# accommodated.

from ctypes import (  # noqa
    c_size_t,
    c_ubyte,
    c_uint64,
    pythonapi,
    Structure,
    Union,
)  # noqa


class FNV(Structure):
    _fields_ = [
        ('prefix', c_size_t),
        ('suffix', c_size_t)
    ]


class SIPHASH(Structure):
    _fields_ = [
        ('k0', c_uint64),
        ('k1', c_uint64),
    ]


class DJBX33A(Structure):
    _fields_ = [
        ('padding', c_ubyte * 16),
        ('suffix', c_size_t),
    ]


class EXPAT(Structure):
    _fields_ = [
        ('padding', c_ubyte * 16),
        ('hashsalt', c_size_t),
    ]


class _Py_HashSecret_t(Union):
    _fields_ = [
        # ensure 24 bytes
        ('uc', c_ubyte * 24),
        # two Py_hash_t for FNV
        ('fnv', FNV),
        # two uint64 for SipHash24
        ('siphash', SIPHASH),
        # a different (!) Py_hash_t for small string optimization
        ('djbx33a', DJBX33A),
        ('expat', EXPAT),
    ]


_hashsecret_entry = namedtuple('_hashsecret_entry', ['symbol', 'value'])


# Only a few members are needed at present
def _build_hashsecret():
    """Read hash secret from the Python process

    Returns
    -------
    info : dict
        - keys are "djbx33a_suffix", "siphash_k0", siphash_k1".
        - values are the namedtuple[symbol:str, value:int]
    """
    # Read hashsecret and inject it into the LLVM symbol map under the
    # prefix `_numba_hashsecret_`.
    pyhashsecret = _Py_HashSecret_t.in_dll(pythonapi, '_Py_HashSecret')
    info = {}

    def inject(name, val):
        symbol_name = "_numba_hashsecret_{}".format(name)
        val = ctypes.c_uint64(val)
        addr = ctypes.addressof(val)
        ll.add_symbol(symbol_name, addr)
        info[name] = _hashsecret_entry(symbol=symbol_name, value=val)

    inject('djbx33a_suffix', pyhashsecret.djbx33a.suffix)
    inject('siphash_k0', pyhashsecret.siphash.k0)
    inject('siphash_k1', pyhashsecret.siphash.k1)
    return info


_hashsecret = _build_hashsecret()


# ------------------------------------------------------------------------------


if _Py_hashfunc_name in ('siphash13', 'siphash24', 'fnv'):

    # Check for use of the FNV hashing alg, warn users that it's not implemented
    # and functionality relying of properties derived from hashing will be fine
    # but hash values themselves are likely to be different.
    if _Py_hashfunc_name == 'fnv':
        msg = ("FNV hashing is not implemented in Numba. See PEP 456 "
               "https://www.python.org/dev/peps/pep-0456/ "
               "for rationale over not using FNV. Numba will continue to work, "
               "but hashes for built in types will be computed using "
               "siphash24. This will permit e.g. dictionaries to continue to "
               "behave as expected, however anything relying on the value of "
               "the hash opposed to hash as a derived property is likely to "
               "not work as expected.")
        warnings.warn(msg)

    # This is a translation of CPython's siphash24 function:
    # https://github.com/python/cpython/blob/d1dd6be613381b996b9071443ef081de8e5f3aff/Python/pyhash.c#L287-L413    # noqa: E501
    # and also, since Py 3.11, a translation of CPython's siphash13 function:
    # https://github.com/python/cpython/blob/9dda9020abcf0d51d59b283a89c58c8e1fb0f574/Python/pyhash.c#L376-L424
    # the only differences are in the use of SINGLE_ROUND in siphash13 vs.
    # DOUBLE_ROUND in siphash24, and that siphash13 has an extra "ROUND" applied
    # just before the final XORing of components to create the return value.

    # /* *********************************************************************
    # <MIT License>
    # Copyright (c) 2013  Marek Majkowski <marek@popcount.org>

    # Permission is hereby granted, free of charge, to any person obtaining a
    # copy of this software and associated documentation files (the "Software"),
    # to deal in the Software without restriction, including without limitation
    # the rights to use, copy, modify, merge, publish, distribute, sublicense,
    # and/or sell copies of the Software, and to permit persons to whom the
    # Software is furnished to do so, subject to the following conditions:

    # The above copyright notice and this permission notice shall be included in
    # all copies or substantial portions of the Software.

    # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
    # THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
    # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
    # DEALINGS IN THE SOFTWARE.
    # </MIT License>

    # Original location:
    # https://github.com/majek/csiphash/

    # Solution inspired by code from:
    # Samuel Neves (supercop/crypto_auth/siphash24/little)
    #djb (supercop/crypto_auth/siphash24/little2)
    # Jean-Philippe Aumasson (https://131002.net/siphash/siphash24.c)

    # Modified for Python by Christian Heimes:
    # - C89 / MSVC compatibility
    # - _rotl64() on Windows
    # - letoh64() fallback
    # */

    @register_jitable(locals={'x': types.uint64,
                              'b': types.uint64, })
    def _ROTATE(x, b):
        return types.uint64(((x) << (b)) | ((x) >> (types.uint64(64) - (b))))

    @register_jitable(locals={'a': types.uint64,
                              'b': types.uint64,
                              'c': types.uint64,
                              'd': types.uint64,
                              's': types.uint64,
                              't': types.uint64, })
    def _HALF_ROUND(a, b, c, d, s, t):
        a += b
        c += d
        b = _ROTATE(b, s) ^ a
        d = _ROTATE(d, t) ^ c
        a = _ROTATE(a, 32)
        return a, b, c, d

    @register_jitable(locals={'v0': types.uint64,
                              'v1': types.uint64,
                              'v2': types.uint64,
                              'v3': types.uint64, })
    def _SINGLE_ROUND(v0, v1, v2, v3):
        v0, v1, v2, v3 = _HALF_ROUND(v0, v1, v2, v3, 13, 16)
        v2, v1, v0, v3 = _HALF_ROUND(v2, v1, v0, v3, 17, 21)
        return v0, v1, v2, v3

    @register_jitable(locals={'v0': types.uint64,
                              'v1': types.uint64,
                              'v2': types.uint64,
                              'v3': types.uint64, })
    def _DOUBLE_ROUND(v0, v1, v2, v3):
        v0, v1, v2, v3 = _SINGLE_ROUND(v0, v1, v2, v3)
        v0, v1, v2, v3 = _SINGLE_ROUND(v0, v1, v2, v3)
        return v0, v1, v2, v3

    def _gen_siphash(alg):
        if alg == 'siphash13':
            _ROUNDER = _SINGLE_ROUND
            _EXTRA_ROUND = True
        elif alg == 'siphash24':
            _ROUNDER = _DOUBLE_ROUND
            _EXTRA_ROUND = False
        else:
            assert 0, 'unreachable'

        @register_jitable(locals={'v0': types.uint64,
                                  'v1': types.uint64,
                                  'v2': types.uint64,
                                  'v3': types.uint64,
                                  'b': types.uint64,
                                  'mi': types.uint64,
                                  't': types.uint64,
                                  'mask': types.uint64,
                                  'jmp': types.uint64,
                                  'ohexefef': types.uint64})
        def _siphash(k0, k1, src, src_sz):
            b = types.uint64(src_sz) << 56
            v0 = k0 ^ types.uint64(0x736f6d6570736575)
            v1 = k1 ^ types.uint64(0x646f72616e646f6d)
            v2 = k0 ^ types.uint64(0x6c7967656e657261)
            v3 = k1 ^ types.uint64(0x7465646279746573)

            idx = 0
            while (src_sz >= 8):
                mi = grab_uint64_t(src, idx)
                idx += 1
                src_sz -= 8
                v3 ^= mi
                v0, v1, v2, v3 = _ROUNDER(v0, v1, v2, v3)
                v0 ^= mi

            # this is the switch fallthrough:
            # https://github.com/python/cpython/blob/d1dd6be613381b996b9071443ef081de8e5f3aff/Python/pyhash.c#L390-L400    # noqa: E501
            t = types.uint64(0x0)
            boffset = idx * 8
            ohexefef = types.uint64(0xff)
            if src_sz >= 7:
                jmp = (6 * 8)
                mask = ~types.uint64(ohexefef << jmp)
                t = (t & mask) | (types.uint64(grab_byte(src, boffset + 6))
                                  << jmp)
            if src_sz >= 6:
                jmp = (5 * 8)
                mask = ~types.uint64(ohexefef << jmp)
                t = (t & mask) | (types.uint64(grab_byte(src, boffset + 5))
                                  << jmp)
            if src_sz >= 5:
                jmp = (4 * 8)
                mask = ~types.uint64(ohexefef << jmp)
                t = (t & mask) | (types.uint64(grab_byte(src, boffset + 4))
                                  << jmp)
            if src_sz >= 4:
                t &= types.uint64(0xffffffff00000000)
                for i in range(4):
                    jmp = i * 8
                    mask = ~types.uint64(ohexefef << jmp)
                    t = (t & mask) | (types.uint64(grab_byte(src, boffset + i))
                                      << jmp)
            if src_sz >= 3:
                jmp = (2 * 8)
                mask = ~types.uint64(ohexefef << jmp)
                t = (t & mask) | (types.uint64(grab_byte(src, boffset + 2))
                                  << jmp)
            if src_sz >= 2:
                jmp = (1 * 8)
                mask = ~types.uint64(ohexefef << jmp)
                t = (t & mask) | (types.uint64(grab_byte(src, boffset + 1))
                                  << jmp)
            if src_sz >= 1:
                mask = ~(ohexefef)
                t = (t & mask) | (types.uint64(grab_byte(src, boffset + 0)))

            b |= t
            v3 ^= b
            v0, v1, v2, v3 = _ROUNDER(v0, v1, v2, v3)
            v0 ^= b
            v2 ^= ohexefef
            v0, v1, v2, v3 = _ROUNDER(v0, v1, v2, v3)
            v0, v1, v2, v3 = _ROUNDER(v0, v1, v2, v3)
            if _EXTRA_ROUND:
                v0, v1, v2, v3 = _ROUNDER(v0, v1, v2, v3)
            t = (v0 ^ v1) ^ (v2 ^ v3)
            return t

        return _siphash

    _siphash13 = _gen_siphash('siphash13')
    _siphash24 = _gen_siphash('siphash24')

    _siphasher = _siphash13 if _Py_hashfunc_name == 'siphash13' else _siphash24

else:
    msg = "Unsupported hashing algorithm in use %s" % _Py_hashfunc_name
    raise ValueError(msg)


@intrinsic
def _inject_hashsecret_read(tyctx, name):
    """Emit code to load the hashsecret.
    """
    if not isinstance(name, types.StringLiteral):
        raise errors.TypingError("requires literal string")

    sym = _hashsecret[name.literal_value].symbol
    resty = types.uint64
    sig = resty(name)

    def impl(cgctx, builder, sig, args):
        mod = builder.module
        try:
            # Search for existing global
            gv = mod.get_global(sym)
        except KeyError:
            # Inject the symbol if not already exist.
            gv = ir.GlobalVariable(mod, ir.IntType(64), name=sym)
        v = builder.load(gv)
        return v

    return sig, impl


def _load_hashsecret(name):
    return _hashsecret[name].value


@overload(_load_hashsecret)
def _impl_load_hashsecret(name):
    def imp(name):
        return _inject_hashsecret_read(name)
    return imp


# This is a translation of CPythons's _Py_HashBytes:
# https://github.com/python/cpython/blob/d1dd6be613381b996b9071443ef081de8e5f3aff/Python/pyhash.c#L145-L191    # noqa: E501


@register_jitable(locals={'_hash': _Py_uhash_t})
def _Py_HashBytes(val, _len):
    if (_len == 0):
        return process_return(0)

    if (_len < _Py_HASH_CUTOFF):
        # TODO: this branch needs testing, needs a CPython setup for it!
        # /* Optimize hashing of very small strings with inline DJBX33A. */
        _hash = _Py_uhash_t(5381)  # /* DJBX33A starts with 5381 */
        for idx in range(_len):
            _hash = ((_hash << 5) + _hash) + np.uint8(grab_byte(val, idx))

        _hash ^= _len
        _hash ^= _load_hashsecret('djbx33a_suffix')
    else:
        tmp = _siphasher(types.uint64(_load_hashsecret('siphash_k0')),
                         types.uint64(_load_hashsecret('siphash_k1')),
                         val, _len)
        _hash = process_return(tmp)
    return process_return(_hash)

# This is an approximate translation of CPython's unicode_hash:
# https://github.com/python/cpython/blob/d1dd6be613381b996b9071443ef081de8e5f3aff/Objects/unicodeobject.c#L11635-L11663    # noqa: E501


@overload_method(types.UnicodeType, '__hash__')
def unicode_hash(val):
    from numba.cpython.unicode import _kind_to_byte_width

    def impl(val):
        kindwidth = _kind_to_byte_width(val._kind)
        _len = len(val)
        # use the cache if possible
        current_hash = val._hash
        if current_hash != -1:
            return current_hash
        else:
            # cannot write hash value to cache in the unicode struct due to
            # pass by value on the struct making the struct member immutable
            return _Py_HashBytes(val._data, kindwidth * _len)

    return impl